diff options
468 files changed, 15106 insertions, 4635 deletions
diff --git a/Android.bp b/Android.bp index aa654865e09b..ed7a4813efe6 100644 --- a/Android.bp +++ b/Android.bp @@ -150,6 +150,9 @@ java_library { visibility: [ // DO NOT ADD ANY MORE ENTRIES TO THIS LIST "//external/robolectric-shadows:__subpackages__", + //This will eventually replace the item above, and serves the + //same purpose. + "//external/robolectric:__subpackages__", "//frameworks/layoutlib:__subpackages__", ], } 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 bdd1fc548af2..048f4a48ab52 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -40,6 +40,8 @@ import android.app.job.JobSnapshot; import android.app.job.JobWorkItem; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -58,6 +60,7 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.BatteryStatsInternal; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.LimitExceededException; import android.os.Looper; @@ -166,6 +169,14 @@ public class JobSchedulerService extends com.android.server.SystemService /** The number of the most recently completed jobs to keep track of for debugging purposes. */ private static final int NUM_COMPLETED_JOB_HISTORY = 20; + /** + * Require the hosting job to specify a network constraint if the included + * {@link android.app.job.JobWorkItem} indicates network usage. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; + @VisibleForTesting public static Clock sSystemClock = Clock.systemUTC(); @@ -3147,7 +3158,11 @@ public class JobSchedulerService extends com.android.server.SystemService return canPersist; } - private void validateJobFlags(JobInfo job, int callingUid) { + private void validateJob(JobInfo job, int callingUid) { + validateJob(job, callingUid, null); + } + + private void validateJob(JobInfo job, int callingUid, @Nullable JobWorkItem jobWorkItem) { job.enforceValidity( CompatChanges.isChangeEnabled( JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid)); @@ -3164,6 +3179,26 @@ public class JobSchedulerService extends com.android.server.SystemService + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job); } } + if (jobWorkItem != null) { + jobWorkItem.enforceValidity(); + if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN + || jobWorkItem.getEstimatedNetworkUploadBytes() + != JobInfo.NETWORK_BYTES_UNKNOWN + || jobWorkItem.getMinimumNetworkChunkBytes() + != JobInfo.NETWORK_BYTES_UNKNOWN) { + if (job.getRequiredNetwork() == null) { + final String errorMsg = "JobWorkItem implies network usage" + + " but job doesn't specify a network constraint"; + if (CompatChanges.isChangeEnabled( + REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS, + callingUid)) { + throw new IllegalArgumentException(errorMsg); + } else { + Slog.e(TAG, errorMsg); + } + } + } + } } // IJobScheduler implementation @@ -3184,7 +3219,7 @@ public class JobSchedulerService extends com.android.server.SystemService } } - validateJobFlags(job, uid); + validateJob(job, uid); final long ident = Binder.clearCallingIdentity(); try { @@ -3212,8 +3247,7 @@ public class JobSchedulerService extends com.android.server.SystemService throw new NullPointerException("work is null"); } - work.enforceValidity(); - validateJobFlags(job, uid); + validateJob(job, uid, work); final long ident = Binder.clearCallingIdentity(); try { @@ -3244,7 +3278,7 @@ public class JobSchedulerService extends com.android.server.SystemService + " not permitted to schedule jobs for other apps"); } - validateJobFlags(job, callerUid); + validateJob(job, callerUid); final long ident = Binder.clearCallingIdentity(); try { diff --git a/api/Android.bp b/api/Android.bp index 9306671d758c..a3e64a565422 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -98,6 +98,7 @@ combined_apis { "framework-configinfrastructure", "framework-connectivity", "framework-connectivity-t", + "framework-devicelock", "framework-federatedcompute", "framework-graphics", "framework-healthconnect", diff --git a/boot/Android.bp b/boot/Android.bp index 9fdb9bc6506a..7839918d6a54 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -72,6 +72,10 @@ platform_bootclasspath { module: "com.android.conscrypt-bootclasspath-fragment", }, { + apex: "com.android.devicelock", + module: "com.android.devicelock-bootclasspath-fragment", + }, + { apex: "com.android.federatedcompute", module: "com.android.federatedcompute-bootclasspath-fragment", }, diff --git a/core/api/current.txt b/core/api/current.txt index 1bfd1e7b50cd..94d199cc00b6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34,6 +34,7 @@ package android { field public static final String BIND_COMPANION_DEVICE_SERVICE = "android.permission.BIND_COMPANION_DEVICE_SERVICE"; field public static final String BIND_CONDITION_PROVIDER_SERVICE = "android.permission.BIND_CONDITION_PROVIDER_SERVICE"; field public static final String BIND_CONTROLS = "android.permission.BIND_CONTROLS"; + field public static final String BIND_CREDENTIAL_PROVIDER_SERVICE = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"; field public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN"; field public static final String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE"; field public static final String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE"; @@ -106,6 +107,7 @@ package android { field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK"; field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS"; field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE"; + field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE"; field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS"; field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"; field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA"; @@ -9844,6 +9846,7 @@ package android.content { field public static final int CONTEXT_RESTRICTED = 4; // 0x4 field public static final String CREDENTIAL_SERVICE = "credential"; field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps"; + field public static final String DEVICE_LOCK_SERVICE = "device_lock"; field public static final String DEVICE_POLICY_SERVICE = "device_policy"; field public static final String DISPLAY_HASH_SERVICE = "display_hash"; field public static final String DISPLAY_SERVICE = "display"; @@ -11990,6 +11993,7 @@ package android.content.pm { field public static final String FEATURE_CONTROLS = "android.software.controls"; field public static final String FEATURE_CREDENTIALS = "android.software.credentials"; field public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin"; + field public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock"; field public static final String FEATURE_EMBEDDED = "android.hardware.type.embedded"; field public static final String FEATURE_ETHERNET = "android.hardware.ethernet"; field public static final String FEATURE_EXPANDED_PICTURE_IN_PICTURE = "android.software.expanded_picture_in_picture"; @@ -39319,6 +39323,7 @@ package android.service.notification { method public final void setNotificationsShown(String[]); method public final void snoozeNotification(String, long); method public final void updateNotificationChannel(@NonNull String, @NonNull android.os.UserHandle, @NonNull android.app.NotificationChannel); + field public static final String ACTION_SETTINGS_HOME = "android.service.notification.action.SETTINGS_HOME"; field public static final int FLAG_FILTER_TYPE_ALERTING = 2; // 0x2 field public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; // 0x1 field public static final int FLAG_FILTER_TYPE_ONGOING = 8; // 0x8 @@ -39326,7 +39331,6 @@ package android.service.notification { field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4 field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1 field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2 - field public static final String INTENT_CATEGORY_SETTINGS_HOME = "android.service.notification.category.SETTINGS_HOME"; field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4 field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1 field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3 @@ -41726,10 +41730,12 @@ package android.telephony { field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long"; 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 public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING = "premium_capability_purchase_url_string"; + field public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = "premium_capability_supported_on_lte_bool"; field public static final String KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL = "prevent_clir_activation_and_deactivation_code_bool"; field public static final String KEY_RADIO_RESTART_FAILURE_CAUSES_INT_ARRAY = "radio_restart_failure_causes_int_array"; field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string"; @@ -44008,7 +44014,7 @@ package android.telephony { field public static final int PHONE_TYPE_GSM = 1; // 0x1 field public static final int PHONE_TYPE_NONE = 0; // 0x0 field public static final int PHONE_TYPE_SIP = 3; // 0x3 - field public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; // 0x1 + field public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; // 0x4 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7 @@ -44016,12 +44022,13 @@ package android.telephony { field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT = 9; // 0x9 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED = 6; // 0x6 - field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; // 0x5 field public static final int SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION = 2; // 0x2 field public static final int SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE = 3; // 0x3 field public static final int SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION = 4; // 0x4 @@ -45403,7 +45410,7 @@ package android.text { method public final int getParagraphLeft(int); method public final int getParagraphRight(int); method public float getPrimaryHorizontal(int); - method @Nullable public android.util.Range<java.lang.Integer> getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy); + method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy); method public float getSecondaryHorizontal(int); method public void getSelectionPath(int, int, android.graphics.Path); method public final float getSpacingAdd(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c170f74b41c3..755e1037ea6b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -145,6 +145,7 @@ package android { field public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL"; field public static final String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW"; field public static final String INVOKE_CARRIER_SETUP = "android.permission.INVOKE_CARRIER_SETUP"; + field public static final String KILL_ALL_BACKGROUND_PROCESSES = "android.permission.KILL_ALL_BACKGROUND_PROCESSES"; field public static final String KILL_UID = "android.permission.KILL_UID"; field public static final String LAUNCH_DEVICE_MANAGER_SETUP = "android.permission.LAUNCH_DEVICE_MANAGER_SETUP"; field public static final String LOCAL_MAC_ADDRESS = "android.permission.LOCAL_MAC_ADDRESS"; @@ -189,6 +190,7 @@ package android { field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER"; field public static final String MANAGE_SPEECH_RECOGNITION = "android.permission.MANAGE_SPEECH_RECOGNITION"; field public static final String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS"; + field public static final String MANAGE_SUBSCRIPTION_USER_ASSOCIATION = "android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION"; field public static final String MANAGE_TEST_NETWORKS = "android.permission.MANAGE_TEST_NETWORKS"; field public static final String MANAGE_TIME_AND_ZONE_DETECTION = "android.permission.MANAGE_TIME_AND_ZONE_DETECTION"; field public static final String MANAGE_UI_TRANSLATION = "android.permission.MANAGE_UI_TRANSLATION"; @@ -2502,11 +2504,49 @@ package android.app.time { field @NonNull public static final android.os.Parcelable.Creator<android.app.time.ExternalTimeSuggestion> CREATOR; } + public final class TimeCapabilities implements android.os.Parcelable { + method public int describeContents(); + method public int getConfigureAutoDetectionEnabledCapability(); + method public int getSetManualTimeCapability(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilities> CREATOR; + } + + public final class TimeCapabilitiesAndConfig implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.app.time.TimeCapabilities getCapabilities(); + method @NonNull public android.app.time.TimeConfiguration getConfiguration(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilitiesAndConfig> CREATOR; + } + + public final class TimeConfiguration implements android.os.Parcelable { + method public int describeContents(); + method public boolean isAutoDetectionEnabled(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeConfiguration> CREATOR; + } + + public static final class TimeConfiguration.Builder { + ctor public TimeConfiguration.Builder(); + ctor public TimeConfiguration.Builder(@NonNull android.app.time.TimeConfiguration); + method @NonNull public android.app.time.TimeConfiguration build(); + method @NonNull public android.app.time.TimeConfiguration.Builder setAutoDetectionEnabled(boolean); + } + public final class TimeManager { method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void addTimeZoneDetectorListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.time.TimeManager.TimeZoneDetectorListener); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull android.app.time.UnixEpochTime); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeState getTimeState(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneState getTimeZoneState(); method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void removeTimeZoneDetectorListener(@NonNull android.app.time.TimeManager.TimeZoneDetectorListener); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull android.app.time.UnixEpochTime); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String); method @RequiresPermission(android.Manifest.permission.SUGGEST_EXTERNAL_TIME) public void suggestExternalTime(@NonNull android.app.time.ExternalTimeSuggestion); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull android.app.time.TimeConfiguration); method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeZoneConfiguration(@NonNull android.app.time.TimeZoneConfiguration); } @@ -2514,10 +2554,19 @@ package android.app.time { method public void onChange(); } + public final class TimeState implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.app.time.UnixEpochTime getUnixEpochTime(); + method public boolean getUserShouldConfirmTime(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeState> CREATOR; + } + public final class TimeZoneCapabilities implements android.os.Parcelable { method public int describeContents(); method public int getConfigureAutoDetectionEnabledCapability(); method public int getConfigureGeoDetectionEnabledCapability(); + method public int getSetManualTimeZoneCapability(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneCapabilities> CREATOR; } @@ -2546,6 +2595,24 @@ package android.app.time { method @NonNull public android.app.time.TimeZoneConfiguration.Builder setGeoDetectionEnabled(boolean); } + public final class TimeZoneState implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getId(); + method public boolean getUserShouldConfirmId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneState> CREATOR; + } + + public final class UnixEpochTime implements android.os.Parcelable { + ctor public UnixEpochTime(long, long); + method @NonNull public android.app.time.UnixEpochTime at(long); + method public int describeContents(); + method public long getElapsedRealtimeMillis(); + method public long getUnixEpochTimeMillis(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.UnixEpochTime> CREATOR; + } + } package android.app.usage { @@ -13324,6 +13391,7 @@ package android.telephony { method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int); method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int); + method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int); method public void requestEmbeddedSubscriptionInfoListRefresh(); method public void requestEmbeddedSubscriptionInfoListRefresh(int); @@ -13334,6 +13402,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean); + method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle); field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED"; field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI; field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI; @@ -13665,6 +13734,7 @@ package android.telephony { field public static final int INVALID_EMERGENCY_NUMBER_DB_VERSION = -1; // 0xffffffff field public static final int KEY_TYPE_EPDG = 1; // 0x1 field public static final int KEY_TYPE_WLAN = 2; // 0x2 + field public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3; // 0x3 field public static final int MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL = 1; // 0x1 field public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2; // 0x2 field public static final int NR_DUAL_CONNECTIVITY_DISABLE = 2; // 0x2 diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index cb7b478d73b4..74329a38057f 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3953,6 +3953,10 @@ public class ActivityManager { * processes to reclaim memory; the system will take care of restarting * these processes in the future as needed. * + * <p class="note">On devices with a {@link Build.VERSION#SECURITY_PATCH} of 2022-12-01 or + * greater, third party applications can only use this API to kill their own processes. + * </p> + * * @param packageName The name of the package whose processes are to * be killed. */ diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 13da1901e559..cc4650a7df71 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -62,6 +62,7 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private boolean mRequireCompatChangeEnabled = true; private boolean mIsAlarmBroadcast = false; + private boolean mIsInteractiveBroadcast = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; @@ -168,6 +169,13 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.is_alarm"; /** + * Corresponds to {@link #setInteractiveBroadcast(boolean)} + * @hide + */ + public static final String KEY_INTERACTIVE_BROADCAST = + "android:broadcast.is_interactive"; + + /** * @hide * @deprecated Use {@link android.os.PowerExemptionManager# * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead. @@ -281,6 +289,7 @@ public class BroadcastOptions extends ComponentOptions { mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true); mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false); + mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false); mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER, IntentFilter.class); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, @@ -599,6 +608,27 @@ public class BroadcastOptions extends ComponentOptions { } /** + * When set, this broadcast will be understood as having originated from + * some direct interaction by the user such as a notification tap or button + * press. Only the OS itself may use this option. + * @hide + * @param broadcastIsInteractive + * @see #isInteractiveBroadcast() + */ + public void setInteractiveBroadcast(boolean broadcastIsInteractive) { + mIsInteractiveBroadcast = broadcastIsInteractive; + } + + /** + * Did this broadcast originate with a direct user interaction? + * @return true if this broadcast is the result of an interaction, false otherwise + * @hide + */ + public boolean isInteractiveBroadcast() { + return mIsInteractiveBroadcast; + } + + /** * Did this broadcast originate from a push message from the server? * * @return true if this broadcast is a push message, false otherwise. @@ -743,6 +773,9 @@ public class BroadcastOptions extends ComponentOptions { if (mIsAlarmBroadcast) { b.putBoolean(KEY_ALARM_BROADCAST, true); } + if (mIsInteractiveBroadcast) { + b.putBoolean(KEY_INTERACTIVE_BROADCAST, true); + } if (mMinManifestReceiverApiLevel != 0) { b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel); } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 4ddfdb603e73..08a6b8c4e135 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -85,6 +85,7 @@ import android.credentials.CredentialManager; import android.credentials.ICredentialManager; import android.debug.AdbManager; import android.debug.IAdbManager; +import android.devicelock.DeviceLockFrameworkInitializer; import android.graphics.fonts.FontManager; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; @@ -1555,6 +1556,7 @@ public final class SystemServiceRegistry { ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers(); NearbyFrameworkInitializer.registerServiceWrappers(); OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); + DeviceLockFrameworkInitializer.registerServiceWrappers(); } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/time/TimeCapabilities.java b/core/java/android/app/time/TimeCapabilities.java index 76bad58e924b..752caac0c5cd 100644 --- a/core/java/android/app/time/TimeCapabilities.java +++ b/core/java/android/app/time/TimeCapabilities.java @@ -20,6 +20,7 @@ import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.time.Capabilities.CapabilityState; import android.os.Parcel; import android.os.Parcelable; @@ -37,6 +38,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeCapabilities implements Parcelable { public static final @NonNull Creator<TimeCapabilities> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/TimeCapabilitiesAndConfig.java b/core/java/android/app/time/TimeCapabilitiesAndConfig.java index b6a081825757..c9a45e04227a 100644 --- a/core/java/android/app/time/TimeCapabilitiesAndConfig.java +++ b/core/java/android/app/time/TimeCapabilitiesAndConfig.java @@ -17,6 +17,7 @@ package android.app.time; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +28,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeCapabilitiesAndConfig implements Parcelable { public static final @NonNull Creator<TimeCapabilitiesAndConfig> CREATOR = diff --git a/core/java/android/app/time/TimeConfiguration.java b/core/java/android/app/time/TimeConfiguration.java index 7d986983160e..048f85a1e1a4 100644 --- a/core/java/android/app/time/TimeConfiguration.java +++ b/core/java/android/app/time/TimeConfiguration.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.StringDef; +import android.annotation.SystemApi; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -40,6 +41,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeConfiguration implements Parcelable { public static final @NonNull Creator<TimeConfiguration> CREATOR = @@ -155,6 +157,7 @@ public final class TimeConfiguration implements Parcelable { * * @hide */ + @SystemApi public static final class Builder { private final Bundle mBundle = new Bundle(); diff --git a/core/java/android/app/time/TimeManager.java b/core/java/android/app/time/TimeManager.java index 9f66f094786b..e35e359424e2 100644 --- a/core/java/android/app/time/TimeManager.java +++ b/core/java/android/app/time/TimeManager.java @@ -88,8 +88,6 @@ public final class TimeManager { /** * Returns the calling user's time capabilities and configuration. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -107,10 +105,26 @@ public final class TimeManager { /** * Modifies the time detection configuration. * - * @return {@code true} if all the configuration settings specified have been set to the - * new values, {@code false} if none have + * <p>The ability to modify configuration settings can be subject to restrictions. For + * example, they may be determined by device hardware, general policy (i.e. only the primary + * user can set them), or by a managed device policy. Use {@link + * #getTimeCapabilitiesAndConfig()} to obtain information at runtime about the user's + * capabilities. + * + * <p>Attempts to modify configuration settings with capabilities that are {@link + * Capabilities#CAPABILITY_NOT_SUPPORTED} or {@link + * Capabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false} + * will be returned. Modifying configuration settings with capabilities that are {@link + * Capabilities#CAPABILITY_NOT_APPLICABLE} or {@link + * Capabilities#CAPABILITY_POSSESSED} will succeed. See {@link + * TimeZoneCapabilities} for further details. * - * @hide + * <p>If the supplied configuration only has some values set, then only the specified settings + * will be updated (where the user's capabilities allow) and other settings will be left + * unchanged. + * + * @return {@code true} if all the configuration settings specified have been set to the + * new values, {@code false} if none have */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull TimeConfiguration configuration) { @@ -280,8 +294,6 @@ public final class TimeManager { /** * Returns a snapshot of the device's current system clock time state. See also {@link * #confirmTime(UnixEpochTime)} for how this information can be used. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -306,8 +318,6 @@ public final class TimeManager { * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time being * confirmed is no longer the time the device is currently set to. Confirming a time * in which the system already has high confidence will return {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull UnixEpochTime unixEpochTime) { @@ -329,8 +339,6 @@ public final class TimeManager { * capabilities prevents the time being accepted, e.g. if the device is currently set to * "automatic time detection". This method returns {@code true} if the time was accepted even * if it is the same as the current device time. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull UnixEpochTime unixEpochTime) { @@ -353,8 +361,6 @@ public final class TimeManager { * Returns a snapshot of the device's current time zone state. See also {@link * #confirmTimeZone(String)} and {@link #setManualTimeZone(String)} for how this information may * be used. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -379,8 +385,6 @@ public final class TimeManager { * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time zone ID being * confirmed is no longer the time zone ID the device is currently set to. Confirming a time * zone ID in which the system already has high confidence returns {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String timeZoneId) { @@ -402,8 +406,6 @@ public final class TimeManager { * capabilities prevents the time zone being accepted, e.g. if the device is currently set to * "automatic time zone detection". {@code true} is returned if the time zone is accepted. A * time zone that is accepted and matches the current device time zone returns {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String timeZoneId) { diff --git a/core/java/android/app/time/TimeState.java b/core/java/android/app/time/TimeState.java index 01c869d99338..c209cde2cf49 100644 --- a/core/java/android/app/time/TimeState.java +++ b/core/java/android/app/time/TimeState.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -36,6 +37,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeState implements Parcelable { public static final @NonNull Creator<TimeState> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java index 2f147cef9ffe..b647fc33055d 100644 --- a/core/java/android/app/time/TimeZoneCapabilities.java +++ b/core/java/android/app/time/TimeZoneCapabilities.java @@ -114,8 +114,6 @@ public final class TimeZoneCapabilities implements Parcelable { * <p>The time zone will be ignored in all cases unless the value is {@link * Capabilities#CAPABILITY_POSSESSED}. See also * {@link TimeZoneConfiguration#isAutoDetectionEnabled()}. - * - * @hide */ @CapabilityState public int getSetManualTimeZoneCapability() { diff --git a/core/java/android/app/time/TimeZoneState.java b/core/java/android/app/time/TimeZoneState.java index 8e87111986ce..beb6dc6d1dfb 100644 --- a/core/java/android/app/time/TimeZoneState.java +++ b/core/java/android/app/time/TimeZoneState.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -36,6 +37,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeZoneState implements Parcelable { public static final @NonNull Creator<TimeZoneState> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java index 576bf6453eca..3a35f3cd1acb 100644 --- a/core/java/android/app/time/UnixEpochTime.java +++ b/core/java/android/app/time/UnixEpochTime.java @@ -19,6 +19,7 @@ package android.app.time; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -38,6 +39,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class UnixEpochTime implements Parcelable { @ElapsedRealtimeLong private final long mElapsedRealtimeMillis; private final long mUnixEpochTimeMillis; @@ -153,9 +155,8 @@ public final class UnixEpochTime implements Parcelable { * Creates a new Unix epoch time value at {@code elapsedRealtimeTimeMillis} by adjusting this * Unix epoch time by the difference between the elapsed realtime value supplied and the one * associated with this instance. - * - * @hide */ + @NonNull public UnixEpochTime at(@ElapsedRealtimeLong long elapsedRealtimeTimeMillis) { long adjustedUnixEpochTimeMillis = (elapsedRealtimeTimeMillis - mElapsedRealtimeMillis) + mUnixEpochTimeMillis; diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index 24e47bf9e47c..2dced96d3583 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -329,6 +329,22 @@ public class AppWidgetHost { } /** + * Set the visibiity of all widgets associated with this host to hidden + * + * @hide + */ + public void setAppWidgetHidden() { + if (sService == null) { + return; + } + try { + sService.setAppWidgetHidden(mContextOpPackageName, mHostId); + } catch (RemoteException e) { + throw new RuntimeException("System server dead?", e); + } + } + + /** * Set the host's interaction handler. * * @hide diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 753c93612f40..d65210b8a0bc 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3938,6 +3938,7 @@ public abstract class Context { //@hide: SAFETY_CENTER_SERVICE, DISPLAY_HASH_SERVICE, CREDENTIAL_SERVICE, + DEVICE_LOCK_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -6073,6 +6074,14 @@ public abstract class Context { public static final String CREDENTIAL_SERVICE = "credential"; /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.devicelock.DeviceLockManager}. + * + * @see #getSystemService(String) + */ + public static final String DEVICE_LOCK_SERVICE = "device_lock"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index db991dcd3afc..823c14281818 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4194,6 +4194,14 @@ public abstract class PackageManager { @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_CREDENTIALS = "android.software.credentials"; + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device supports locking (for example, by a financing provider in case of a missed + * payment). + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock"; + /** @hide */ public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true; diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java new file mode 100644 index 000000000000..cf5f0363208a --- /dev/null +++ b/core/java/android/credentials/ui/BaseDialogResult.java @@ -0,0 +1,123 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +/** + * Base dialog result data. + * + * Returned for simple use cases like cancellation. Can also be subclassed when more information + * is needed, e.g. {@link UserSelectionDialogResult}. + * + * @hide + */ +public class BaseDialogResult implements Parcelable { + /** Parses and returns a BaseDialogResult from the given resultData. */ + @Nullable + public static BaseDialogResult fromResultData(@NonNull Bundle resultData) { + return resultData.getParcelable(EXTRA_BASE_RESULT, BaseDialogResult.class); + } + + /** + * Used for the UX to construct the {@code resultData Bundle} to send via the {@code + * ResultReceiver}. + */ + public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) { + bundle.putParcelable(EXTRA_BASE_RESULT, result); + } + + /** + * The intent extra key for the {@code BaseDialogResult} object when the credential + * selector activity finishes. + */ + private static final String EXTRA_BASE_RESULT = + "android.credentials.ui.extra.BASE_RESULT"; + + /** User intentionally canceled the dialog. */ + public static final int RESULT_CODE_DIALOG_CANCELED = 0; + /** + * User made a selection and the dialog finished. The user selection result is in the + * {@code resultData}. + */ + public static final int RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION = 1; + /** + * The user has acknowledged the consent page rendered for when they first used Credential + * Manager on this device. + */ + public static final int RESULT_CODE_CREDENTIAL_MANAGER_CONSENT_ACKNOWLEDGED = 2; + /** + * The user has acknowledged the consent page rendered for enabling a new provider. + * This should only happen during the first time use. The provider info is in the + * {@code resultData}. + */ + public static final int RESULT_CODE_PROVIDER_ENABLED = 3; + /** + * The user has consented to switching to a new default provider. The provider info is in the + * {@code resultData}. + */ + public static final int RESULT_CODE_DEFAULT_PROVIDER_CHANGED = 4; + + @NonNull + private final IBinder mRequestToken; + + public BaseDialogResult(@NonNull IBinder requestToken) { + mRequestToken = requestToken; + } + + /** Returns the unique identifier for the request that launched the operation. */ + @NonNull + public IBinder getRequestToken() { + return mRequestToken; + } + + protected BaseDialogResult(@NonNull Parcel in) { + IBinder requestToken = in.readStrongBinder(); + mRequestToken = requestToken; + AnnotationValidations.validate(NonNull.class, null, mRequestToken); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeStrongBinder(mRequestToken); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<BaseDialogResult> CREATOR = + new Creator<BaseDialogResult>() { + @Override + public BaseDialogResult createFromParcel(@NonNull Parcel in) { + return new BaseDialogResult(in); + } + + @Override + public BaseDialogResult[] newArray(int size) { + return new BaseDialogResult[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java index aeeede744188..53ad40df2252 100644 --- a/core/java/android/credentials/ui/Constants.java +++ b/core/java/android/credentials/ui/Constants.java @@ -29,5 +29,4 @@ public class Constants { */ public static final String EXTRA_RESULT_RECEIVER = "android.credentials.ui.extra.RESULT_RECEIVER"; - } diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 122c54ad8144..b9ee72dcdcf8 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -30,12 +30,39 @@ import com.android.internal.util.AnnotationValidations; * @hide */ public class Entry implements Parcelable { - // TODO: move to jetpack. + // TODO: these constants should go to jetpack. public static final String VERSION = "v1"; public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice"); - public static final String HINT_TITLE = "hint_title"; - public static final String HINT_SUBTITLE = "hint_subtitle"; - public static final String HINT_ICON = "hint_icon"; + // TODO: remove these hint constants and use the credential entry & action ones defined below. + public static final String HINT_TITLE = "HINT_TITLE"; + public static final String HINT_SUBTITLE = "HINT_SUBTITLE"; + public static final String HINT_ICON = "HINT_ICON"; + /** + * 1. CREDENTIAL ENTRY CONSTANTS + */ + // User profile picture associated with this credential entry. + public static final String HINT_PROFILE_ICON = "HINT_PROFILE_ICON"; + public static final String HINT_CREDENTIAL_TYPE_ICON = "HINT_CREDENTIAL_TYPE_ICON"; + // The user account name of this provider app associated with this entry. + // Note: this is independent from the request app. + public static final String HINT_USER_PROVIDER_ACCOUNT_NAME = "HINT_USER_PROVIDER_ACCOUNT_NAME"; + public static final String HINT_PASSWORD_COUNT = "HINT_PASSWORD_COUNT"; + public static final String HINT_PASSKEY_COUNT = "HINT_PASSKEY_COUNT"; + public static final String HINT_TOTAL_CREDENTIAL_COUNT = "HINT_TOTAL_CREDENTIAL_COUNT"; + public static final String HINT_LAST_USED_TIME_MILLIS = "HINT_LAST_USED_TIME_MILLIS"; + /** Below are only available for get flows. */ + public static final String HINT_NOTE = "HINT_NOTE"; + public static final String HINT_USER_NAME = "HINT_USER_NAME"; + public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE"; + public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME"; + public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE"; + + /** + * 2. ACTION CONSTANTS + */ + public static final String HINT_ACTION_TITLE = "HINT_ACTION_TITLE"; + public static final String HINT_ACTION_SUBTEXT = "HINT_ACTION_SUBTEXT"; + public static final String HINT_ACTION_ICON = "HINT_ACTION_ICON"; /** * The intent extra key for the action chip {@code Entry} list when launching the UX activities. @@ -55,7 +82,7 @@ public class Entry implements Parcelable { public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION = "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION"; - // TODO: may be changed to other type depending on the service implementation. + // TODO: change to string key + string subkey. private final int mId; @NonNull diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java index 9a038d137434..1b70ea4ebd71 100644 --- a/core/java/android/credentials/ui/IntentFactory.java +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -34,8 +34,7 @@ public class IntentFactory { ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) { Intent intent = new Intent(); // TODO: define these as proper config strings. - String activityName = "com.androidauth.tatiaccountselector/.CredentialSelectorActivity"; - // String activityName = "com.android.credentialmanager/.CredentialSelectorActivity"; + String activityName = "com.android.credentialmanager/.CredentialSelectorActivity"; intent.setComponent(ComponentName.unflattenFromString(activityName)); intent.putParcelableArrayListExtra( diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java new file mode 100644 index 000000000000..9d1be2063423 --- /dev/null +++ b/core/java/android/credentials/ui/ProviderDialogResult.java @@ -0,0 +1,100 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +/** + * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link + * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}. + * + * @hide + */ +public class ProviderDialogResult extends BaseDialogResult implements Parcelable { + /** Parses and returns a ProviderDialogResult from the given resultData. */ + @Nullable + public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) { + return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class); + } + + /** + * Used for the UX to construct the {@code resultData Bundle} to send via the {@code + * ResultReceiver}. + */ + public static void addToBundle( + @NonNull ProviderDialogResult result, @NonNull Bundle bundle) { + bundle.putParcelable(EXTRA_PROVIDER_RESULT, result); + } + + /** + * The intent extra key for the {@code ProviderDialogResult} object when the credential + * selector activity finishes. + */ + private static final String EXTRA_PROVIDER_RESULT = + "android.credentials.ui.extra.PROVIDER_RESULT"; + + @NonNull + private final String mProviderId; + + public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) { + super(requestToken); + mProviderId = providerId; + } + + @NonNull + public String getProviderId() { + return mProviderId; + } + + protected ProviderDialogResult(@NonNull Parcel in) { + super(in); + String providerId = in.readString8(); + mProviderId = providerId; + AnnotationValidations.validate(NonNull.class, null, mProviderId); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString8(mProviderId); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<ProviderDialogResult> CREATOR = + new Creator<ProviderDialogResult>() { + @Override + public ProviderDialogResult createFromParcel(@NonNull Parcel in) { + return new ProviderDialogResult(in); + } + + @Override + public ProviderDialogResult[] newArray(int size) { + return new ProviderDialogResult[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index eddb519051a9..619b08ec9ca7 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -17,12 +17,19 @@ package android.credentials.ui; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialRequest; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Contains information about the request that initiated this UX flow. * @@ -42,18 +49,45 @@ public class RequestInfo implements Parcelable { /** Type value for an executeCreateCredential request. */ public static final @NonNull String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE"; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef(value = { TYPE_GET, TYPE_CREATE }) + public @interface RequestType {} + @NonNull private final IBinder mToken; + @Nullable + private final CreateCredentialRequest mCreateCredentialRequest; + + @Nullable + private final GetCredentialRequest mGetCredentialRequest; + @NonNull + @RequestType private final String mType; private final boolean mIsFirstUsage; - public RequestInfo(@NonNull IBinder token, @NonNull String type, boolean isFirstUsage) { - mToken = token; - mType = type; - mIsFirstUsage = isFirstUsage; + @NonNull + private final String mAppDisplayName; + + /** Creates new {@code RequestInfo} for a create-credential flow. */ + public static RequestInfo newCreateRequestInfo( + @NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest, + boolean isFirstUsage, @NonNull String appDisplayName) { + return new RequestInfo( + token, TYPE_CREATE, isFirstUsage, appDisplayName, + createCredentialRequest, null); + } + + /** Creates new {@code RequestInfo} for a get-credential flow. */ + public static RequestInfo newGetRequestInfo( + @NonNull IBinder token, @NonNull GetCredentialRequest getCredentialRequest, + boolean isFirstUsage, @NonNull String appDisplayName) { + return new RequestInfo( + token, TYPE_GET, isFirstUsage, appDisplayName, + null, getCredentialRequest); } /** Returns the request token matching the user request. */ @@ -64,6 +98,7 @@ public class RequestInfo implements Parcelable { /** Returns the request type. */ @NonNull + @RequestType public String getType() { return mType; } @@ -78,16 +113,61 @@ public class RequestInfo implements Parcelable { return mIsFirstUsage; } + /** Returns the display name of the app that made this request. */ + @NonNull + public String getAppDisplayName() { + return mAppDisplayName; + } + + /** + * Returns the non-null CreateCredentialRequest when the type of the request is {@link + * #TYPE_CREATE}, or null otherwise. + */ + @Nullable + public CreateCredentialRequest getCreateCredentialRequest() { + return mCreateCredentialRequest; + } + + /** + * Returns the non-null GetCredentialRequest when the type of the request is {@link + * #TYPE_GET}, or null otherwise. + */ + @Nullable + public GetCredentialRequest getGetCredentialRequest() { + return mGetCredentialRequest; + } + + private RequestInfo(@NonNull IBinder token, @NonNull @RequestType String type, + boolean isFirstUsage, @NonNull String appDisplayName, + @Nullable CreateCredentialRequest createCredentialRequest, + @Nullable GetCredentialRequest getCredentialRequest) { + mToken = token; + mType = type; + mIsFirstUsage = isFirstUsage; + mAppDisplayName = appDisplayName; + mCreateCredentialRequest = createCredentialRequest; + mGetCredentialRequest = getCredentialRequest; + } + protected RequestInfo(@NonNull Parcel in) { IBinder token = in.readStrongBinder(); String type = in.readString8(); boolean isFirstUsage = in.readBoolean(); + String appDisplayName = in.readString8(); + CreateCredentialRequest createCredentialRequest = + in.readTypedObject(CreateCredentialRequest.CREATOR); + GetCredentialRequest getCredentialRequest = + in.readTypedObject(GetCredentialRequest.CREATOR); mToken = token; AnnotationValidations.validate(NonNull.class, null, mToken); mType = type; AnnotationValidations.validate(NonNull.class, null, mType); mIsFirstUsage = isFirstUsage; + mAppDisplayName = appDisplayName; + AnnotationValidations.validate(NonNull.class, null, mAppDisplayName); + mCreateCredentialRequest = createCredentialRequest; + mGetCredentialRequest = getCredentialRequest; } @Override @@ -95,6 +175,9 @@ public class RequestInfo implements Parcelable { dest.writeStrongBinder(mToken); dest.writeString8(mType); dest.writeBoolean(mIsFirstUsage); + dest.writeString8(mAppDisplayName); + dest.writeTypedObject(mCreateCredentialRequest, flags); + dest.writeTypedObject(mGetCredentialRequest, flags); } @Override diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionDialogResult.java index 2ac559381c6e..eb3a4a8cfcbd 100644 --- a/core/java/android/credentials/ui/UserSelectionResult.java +++ b/core/java/android/credentials/ui/UserSelectionDialogResult.java @@ -17,6 +17,8 @@ package android.credentials.ui; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -24,24 +26,33 @@ import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; /** - * User selection result information of a UX flow. - * - * Returned as part of the activity result intent data when the user dialog completes - * successfully. + * Result data matching {@link BaseDialogResult#RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION}. * * @hide */ -public class UserSelectionResult implements Parcelable { +public class UserSelectionDialogResult extends BaseDialogResult implements Parcelable { + /** Parses and returns a UserSelectionDialogResult from the given resultData. */ + @Nullable + public static UserSelectionDialogResult fromResultData(@NonNull Bundle resultData) { + return resultData.getParcelable( + EXTRA_USER_SELECTION_RESULT, UserSelectionDialogResult.class); + } /** - * The intent extra key for the {@code UserSelectionResult} object when the credential selector - * activity finishes. - */ - public static final String EXTRA_USER_SELECTION_RESULT = - "android.credentials.ui.extra.USER_SELECTION_RESULT"; + * Used for the UX to construct the {@code resultData Bundle} to send via the {@code + * ResultReceiver}. + */ + public static void addToBundle( + @NonNull UserSelectionDialogResult result, @NonNull Bundle bundle) { + bundle.putParcelable(EXTRA_USER_SELECTION_RESULT, result); + } - @NonNull - private final IBinder mRequestToken; + /** + * The intent extra key for the {@code UserSelectionDialogResult} object when the credential + * selector activity finishes. + */ + private static final String EXTRA_USER_SELECTION_RESULT = + "android.credentials.ui.extra.USER_SELECTION_RESULT"; @NonNull private final String mProviderId; @@ -49,19 +60,14 @@ public class UserSelectionResult implements Parcelable { // TODO: consider switching to string or other types, depending on the service implementation. private final int mEntryId; - public UserSelectionResult(@NonNull IBinder requestToken, @NonNull String providerId, + public UserSelectionDialogResult( + @NonNull IBinder requestToken, @NonNull String providerId, int entryId) { - mRequestToken = requestToken; + super(requestToken); mProviderId = providerId; mEntryId = entryId; } - /** Returns token of the app request that initiated this user dialog. */ - @NonNull - public IBinder getRequestToken() { - return mRequestToken; - } - /** Returns provider package name whose entry was selected by the user. */ @NonNull public String getProviderId() { @@ -73,13 +79,11 @@ public class UserSelectionResult implements Parcelable { return mEntryId; } - protected UserSelectionResult(@NonNull Parcel in) { - IBinder requestToken = in.readStrongBinder(); + protected UserSelectionDialogResult(@NonNull Parcel in) { + super(in); String providerId = in.readString8(); int entryId = in.readInt(); - mRequestToken = requestToken; - AnnotationValidations.validate(NonNull.class, null, mRequestToken); mProviderId = providerId; AnnotationValidations.validate(NonNull.class, null, mProviderId); mEntryId = entryId; @@ -87,7 +91,7 @@ public class UserSelectionResult implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeStrongBinder(mRequestToken); + super.writeToParcel(dest, flags); dest.writeString8(mProviderId); dest.writeInt(mEntryId); } @@ -97,16 +101,16 @@ public class UserSelectionResult implements Parcelable { return 0; } - public static final @NonNull Creator<UserSelectionResult> CREATOR = - new Creator<UserSelectionResult>() { + public static final @NonNull Creator<UserSelectionDialogResult> CREATOR = + new Creator<UserSelectionDialogResult>() { @Override - public UserSelectionResult createFromParcel(@NonNull Parcel in) { - return new UserSelectionResult(in); + public UserSelectionDialogResult createFromParcel(@NonNull Parcel in) { + return new UserSelectionDialogResult(in); } @Override - public UserSelectionResult[] newArray(int size) { - return new UserSelectionResult[size]; + public UserSelectionDialogResult[] newArray(int size) { + return new UserSelectionDialogResult[size]; } }; } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index dff2f7ed1cf3..50551feed522 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -133,9 +133,6 @@ public final class CameraManager { private HandlerThread mHandlerThread; private Handler mHandler; private FoldStateListener mFoldStateListener; - @GuardedBy("mLock") - private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>(); - private boolean mFoldedDeviceState; /** * @hide @@ -144,31 +141,39 @@ public final class CameraManager { void onDeviceStateChanged(boolean folded); } - private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { + private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { private final int[] mFoldedDeviceStates; + private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = + new ArrayList<>(); + private boolean mFoldedDeviceState; + public FoldStateListener(Context context) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); } - private void handleStateChange(int state) { + private synchronized void handleStateChange(int state) { boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); - synchronized (mLock) { - mFoldedDeviceState = folded; - ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>(); - for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) { - DeviceStateListener callback = listener.get(); - if (callback != null) { - callback.onDeviceStateChanged(folded); - } else { - invalidListeners.add(listener); - } - } - if (!invalidListeners.isEmpty()) { - mDeviceStateListeners.removeAll(invalidListeners); + + mFoldedDeviceState = folded; + ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>(); + for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) { + DeviceStateListener callback = listener.get(); + if (callback != null) { + callback.onDeviceStateChanged(folded); + } else { + invalidListeners.add(listener); } } + if (!invalidListeners.isEmpty()) { + mDeviceStateListeners.removeAll(invalidListeners); + } + } + + public synchronized void addDeviceStateListener(DeviceStateListener listener) { + listener.onDeviceStateChanged(mFoldedDeviceState); + mDeviceStateListeners.add(new WeakReference<>(listener)); } @Override @@ -192,9 +197,8 @@ public final class CameraManager { public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) { synchronized (mLock) { DeviceStateListener listener = chars.getDeviceStateListener(); - listener.onDeviceStateChanged(mFoldedDeviceState); if (mFoldStateListener != null) { - mDeviceStateListeners.add(new WeakReference<>(listener)); + mFoldStateListener.addDeviceStateListener(listener); } } } diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 7247ef77afb4..197739b6a067 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -768,6 +768,20 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } } + /** + * Schedules a watchdog. + * + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void scheduleWatchdog() { + try { + mService.scheduleWatchdog(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private void cancelEnrollment(long requestId) { if (mService != null) { try { diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 9b56f43a0f22..2bf187ac9006 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -172,4 +172,9 @@ interface IFaceService { // Registers BiometricStateListener. void registerBiometricStateListener(IBiometricStateListener listener); + + // Internal operation used to clear face biometric scheduler. + // Ensures that the scheduler is not stuck. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void scheduleWatchdog(); } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 0fd164de8ffb..5403f089b308 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -1080,7 +1080,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing */ public boolean isPowerbuttonFps() { final FingerprintSensorPropertiesInternal sensorProps = getFirstFingerprintSensor(); - return sensorProps.sensorType == TYPE_POWER_BUTTON; + return sensorProps == null ? false : sensorProps.sensorType == TYPE_POWER_BUTTON; } /** @@ -1125,6 +1125,20 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** + * Schedules a watchdog. + * + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void scheduleWatchdog() { + try { + mService.scheduleWatchdog(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide */ public void addLockoutResetCallback(final LockoutResetCallback callback) { diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index 1ba9a0471c88..051e3a4caa4e 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -208,4 +208,9 @@ interface IFingerprintService { // Sends a power button pressed event to all listeners. @EnforcePermission("USE_BIOMETRIC_INTERNAL") oneway void onPowerPressed(); + + // Internal operation used to clear fingerprint biometric scheduler. + // Ensures that the scheduler is not stuck. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void scheduleWatchdog(); } diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java index 8febed3fb2a0..3ba3ebceeb18 100644 --- a/core/java/android/hardware/radio/Announcement.java +++ b/core/java/android/hardware/radio/Announcement.java @@ -85,9 +85,9 @@ public final class Announcement implements Parcelable { /** @hide */ public Announcement(@NonNull ProgramSelector selector, @Type int type, @NonNull Map<String, String> vendorInfo) { - mSelector = Objects.requireNonNull(selector); - mType = Objects.requireNonNull(type); - mVendorInfo = Objects.requireNonNull(vendorInfo); + mSelector = Objects.requireNonNull(selector, "Program selector cannot be null"); + mType = type; + mVendorInfo = Objects.requireNonNull(vendorInfo, "Vendor info cannot be null"); } private Announcement(@NonNull Parcel in) { diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index f2525d17e30a..ade9fd68ade8 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -160,6 +160,7 @@ public final class ProgramList implements AutoCloseable { * Disables list updates and releases all resources. */ public void close() { + OnCloseListener onCompleteListenersCopied = null; synchronized (mLock) { if (mIsClosed) return; mIsClosed = true; @@ -167,10 +168,14 @@ public final class ProgramList implements AutoCloseable { mListCallbacks.clear(); mOnCompleteListeners.clear(); if (mOnCloseListener != null) { - mOnCloseListener.onClose(); + onCompleteListenersCopied = mOnCloseListener; mOnCloseListener = null; } } + + if (onCompleteListenersCopied != null) { + onCompleteListenersCopied.onClose(); + } } void apply(Chunk chunk) { diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS index 1f686e5c449c..c80c57ce917a 100644 --- a/core/java/android/os/storage/OWNERS +++ b/core/java/android/os/storage/OWNERS @@ -1,11 +1,15 @@ # Bug component: 95221 +# Android Storage Team +abkaur@google.com corinac@google.com -nandana@google.com -zezeozue@google.com -maco@google.com +dipankarb@google.com +krishang@google.com sahanas@google.com -abkaur@google.com -chiangi@google.com +sergeynv@google.com +shubhisaxena@google.com +tylersaunders@google.com + +maco@google.com +nandana@google.com narayan@google.com -dipankarb@google.com diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4e15b38463d6..29e24598f874 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6875,6 +6875,14 @@ public final class Settings { @Readable public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; + + /** + * The currently selected credential service(s) flattened ComponentName. + * + * @hide + */ + public static final String CREDENTIAL_SERVICE = "credential_service"; + /** * The currently selected autofill service flattened ComponentName. * @hide diff --git a/core/java/android/service/credentials/Action.java b/core/java/android/service/credentials/Action.java index e2c11fbac008..553a32419533 100644 --- a/core/java/android/service/credentials/Action.java +++ b/core/java/android/service/credentials/Action.java @@ -50,9 +50,8 @@ public final class Action implements Parcelable { } private Action(@NonNull Parcel in) { - mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class); - mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(), - PendingIntent.class); + mSlice = in.readTypedObject(Slice.CREATOR); + mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); } public static final @NonNull Creator<Action> CREATOR = new Creator<Action>() { @@ -74,8 +73,8 @@ public final class Action implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - mSlice.writeToParcel(dest, flags); - mPendingIntent.writeToParcel(dest, flags); + dest.writeTypedObject(mSlice, flags); + dest.writeTypedObject(mPendingIntent, flags); } /** diff --git a/core/java/android/service/credentials/CreateCredentialRequest.java b/core/java/android/service/credentials/CreateCredentialRequest.java index 6a0bbc0bd917..e6da349a2fbe 100644 --- a/core/java/android/service/credentials/CreateCredentialRequest.java +++ b/core/java/android/service/credentials/CreateCredentialRequest.java @@ -54,7 +54,7 @@ public final class CreateCredentialRequest implements Parcelable { private CreateCredentialRequest(@NonNull Parcel in) { mCallingPackage = in.readString8(); mType = in.readString8(); - mData = in.readBundle(); + mData = in.readTypedObject(Bundle.CREATOR); } public static final @NonNull Creator<CreateCredentialRequest> CREATOR = @@ -79,7 +79,7 @@ public final class CreateCredentialRequest implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mCallingPackage); dest.writeString8(mType); - dest.writeBundle(mData); + dest.writeTypedObject(mData, flags); } /** Returns the calling package of the calling app. */ diff --git a/core/java/android/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java index 613eba8c9bb2..559b1caab87c 100644 --- a/core/java/android/service/credentials/CreateCredentialResponse.java +++ b/core/java/android/service/credentials/CreateCredentialResponse.java @@ -38,7 +38,9 @@ public final class CreateCredentialResponse implements Parcelable { private CreateCredentialResponse(@NonNull Parcel in) { mHeader = in.readCharSequence(); - mSaveEntries = in.createTypedArrayList(SaveEntry.CREATOR); + List<SaveEntry> saveEntries = new ArrayList<>(); + in.readTypedList(saveEntries, SaveEntry.CREATOR); + mSaveEntries = saveEntries; } @Override diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 49b84359d94a..4cc43a10e88f 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -65,12 +65,10 @@ public final class CredentialEntry implements Parcelable { } private CredentialEntry(@NonNull Parcel in) { - mType = in.readString(); - mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class); - mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(), - PendingIntent.class); - mCredential = in.readParcelable(Credential.class.getClassLoader(), - Credential.class); + mType = in.readString8(); + mSlice = in.readTypedObject(Slice.CREATOR); + mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); + mCredential = in.readTypedObject(Credential.CREATOR); mAutoSelectAllowed = in.readBoolean(); } @@ -95,9 +93,9 @@ public final class CredentialEntry implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mType); - mSlice.writeToParcel(dest, flags); - mPendingIntent.writeToParcel(dest, flags); - mCredential.writeToParcel(dest, flags); + dest.writeTypedObject(mSlice, flags); + dest.writeTypedObject(mPendingIntent, flags); + dest.writeTypedObject(mCredential, flags); dest.writeBoolean(mAutoSelectAllowed); } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java new file mode 100644 index 000000000000..e3f8cb7bb23e --- /dev/null +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.credentials; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * {@link ServiceInfo} and meta-data about a credential provider. + * + * @hide + */ +public final class CredentialProviderInfo { + private static final String TAG = "CredentialProviderInfo"; + + @NonNull + private final ServiceInfo mServiceInfo; + @NonNull + private final List<String> mCapabilities; + + // TODO: Move the two strings below to CredentialProviderService when ready. + private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; + private static final String SERVICE_INTERFACE = + "android.service.credentials.CredentialProviderService"; + + + /** + * Constructs an information instance of the credential provider. + * + * @param context The context object + * @param serviceComponent The serviceComponent of the provider service + * @param userId The android userId for which the current process is running + * @throws PackageManager.NameNotFoundException If provider service is not found + */ + public CredentialProviderInfo(@NonNull Context context, + @NonNull ComponentName serviceComponent, int userId) + throws PackageManager.NameNotFoundException { + this(context, getServiceInfoOrThrow(serviceComponent, userId)); + } + + private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { + Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + + "does not require permission" + + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); + throw new SecurityException("Service does not require the expected permission : " + + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); + } + mServiceInfo = serviceInfo; + mCapabilities = new ArrayList<>(); + populateProviderCapabilities(context); + } + + private void populateProviderCapabilities(@NonNull Context context) { + if (mServiceInfo.applicationInfo.metaData == null) { + return; + } + try { + final int resourceId = mServiceInfo.applicationInfo.metaData.getInt( + CAPABILITY_META_DATA_KEY); + String[] capabilities = context.getResources().getStringArray(resourceId); + if (capabilities == null) { + Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName); + return; + } + for (String capability : capabilities) { + if (capability.isEmpty()) { + Log.w(TAG, "Skipping empty capability"); + continue; + } + mCapabilities.add(capability); + } + } catch (Resources.NotFoundException e) { + Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage()); + } + } + + private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent, + int userId) throws PackageManager.NameNotFoundException { + try { + ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.GET_META_DATA, + userId); + if (si != null) { + return si; + } + } catch (RemoteException e) { + Slog.v(TAG, e.getMessage()); + } + throw new PackageManager.NameNotFoundException(serviceComponent.toString()); + } + + /** + * Returns true if the service supports the given {@code credentialType}, false otherwise. + */ + @NonNull + public boolean hasCapability(@NonNull String credentialType) { + return mCapabilities.contains(credentialType); + } + + /** Returns the service info. */ + @NonNull + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + /** Returns an immutable list of capabilities this provider service can support. */ + @NonNull + public List<String> getCapabilities() { + return Collections.unmodifiableList(mCapabilities); + } + + /** + * Returns the valid credential provider services available for the user with the + * given {@code userId}. + */ + public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context, + @UserIdInt int userId) { + final List<CredentialProviderInfo> services = new ArrayList<>(); + + final List<ResolveInfo> resolveInfos = + context.getPackageManager().queryIntentServicesAsUser( + new Intent(SERVICE_INTERFACE), + PackageManager.GET_META_DATA, + userId); + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + try { + services.add(new CredentialProviderInfo(context, serviceInfo)); + } catch (SecurityException e) { + Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e); + } + } + return services; + } + + /** + * Returns the valid credential provider services available for the user, that can + * support the given {@code credentialType}. + */ + public static List<CredentialProviderInfo> getAvailableServicesForCapability( + Context context, @UserIdInt int userId, String credentialType) { + List<CredentialProviderInfo> servicesForCapability = new ArrayList<>(); + final List<CredentialProviderInfo> services = getAvailableServices(context, userId); + + for (CredentialProviderInfo service : services) { + if (service.hasCapability(credentialType)) { + servicesForCapability.add(service); + } + } + return servicesForCapability; + } +} diff --git a/core/java/android/service/credentials/CredentialsDisplayContent.java b/core/java/android/service/credentials/CredentialsDisplayContent.java index 4133ea5955c4..2cce169e7a58 100644 --- a/core/java/android/service/credentials/CredentialsDisplayContent.java +++ b/core/java/android/service/credentials/CredentialsDisplayContent.java @@ -53,8 +53,12 @@ public final class CredentialsDisplayContent implements Parcelable { private CredentialsDisplayContent(@NonNull Parcel in) { mHeader = in.readCharSequence(); - mCredentialEntries = in.createTypedArrayList(CredentialEntry.CREATOR); - mActions = in.createTypedArrayList(Action.CREATOR); + List<CredentialEntry> credentialEntries = new ArrayList<>(); + in.readTypedList(credentialEntries, CredentialEntry.CREATOR); + mCredentialEntries = credentialEntries; + List<Action> actions = new ArrayList<>(); + in.readTypedList(actions, Action.CREATOR); + mActions = actions; } public static final @NonNull Creator<CredentialsDisplayContent> CREATOR = @@ -78,8 +82,8 @@ public final class CredentialsDisplayContent implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeCharSequence(mHeader); - dest.writeTypedList(mCredentialEntries); - dest.writeTypedList(mActions); + dest.writeTypedList(mCredentialEntries, flags); + dest.writeTypedList(mActions, flags); } /** diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java index 5b1a1713ee51..e06be4433062 100644 --- a/core/java/android/service/credentials/GetCredentialsRequest.java +++ b/core/java/android/service/credentials/GetCredentialsRequest.java @@ -49,8 +49,10 @@ public final class GetCredentialsRequest implements Parcelable { } private GetCredentialsRequest(@NonNull Parcel in) { - mCallingPackage = in.readString16NoHelper(); - mGetCredentialOptions = in.createTypedArrayList(GetCredentialOption.CREATOR); + mCallingPackage = in.readString8(); + List<GetCredentialOption> getCredentialOptions = new ArrayList<>(); + in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR); + mGetCredentialOptions = getCredentialOptions; } public static final @NonNull Creator<GetCredentialsRequest> CREATOR = @@ -73,7 +75,7 @@ public final class GetCredentialsRequest implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString16NoHelper(mCallingPackage); + dest.writeString8(mCallingPackage); dest.writeTypedList(mGetCredentialOptions); } diff --git a/core/java/android/service/credentials/GetCredentialsResponse.java b/core/java/android/service/credentials/GetCredentialsResponse.java index 980d9ae47aa7..979a6993c3d4 100644 --- a/core/java/android/service/credentials/GetCredentialsResponse.java +++ b/core/java/android/service/credentials/GetCredentialsResponse.java @@ -78,9 +78,8 @@ public final class GetCredentialsResponse implements Parcelable { } private GetCredentialsResponse(@NonNull Parcel in) { - mCredentialsDisplayContent = in.readParcelable(CredentialsDisplayContent.class - .getClassLoader(), CredentialsDisplayContent.class); - mAuthenticationAction = in.readParcelable(Action.class.getClassLoader(), Action.class); + mCredentialsDisplayContent = in.readTypedObject(CredentialsDisplayContent.CREATOR); + mAuthenticationAction = in.readTypedObject(Action.CREATOR); } public static final @NonNull Creator<GetCredentialsResponse> CREATOR = @@ -103,8 +102,8 @@ public final class GetCredentialsResponse implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeParcelable(mCredentialsDisplayContent, flags); - dest.writeParcelable(mAuthenticationAction, flags); + dest.writeTypedObject(mCredentialsDisplayContent, flags); + dest.writeTypedObject(mAuthenticationAction, flags); } /** diff --git a/core/java/android/service/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java index 18644f09ecef..abe51d43bc48 100644 --- a/core/java/android/service/credentials/SaveEntry.java +++ b/core/java/android/service/credentials/SaveEntry.java @@ -40,10 +40,9 @@ public final class SaveEntry implements Parcelable { private final @Nullable Credential mCredential; private SaveEntry(@NonNull Parcel in) { - mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class); - mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(), - PendingIntent.class); - mCredential = in.readParcelable(Credential.class.getClassLoader(), Credential.class); + mSlice = in.readTypedObject(Slice.CREATOR); + mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); + mCredential = in.readTypedObject(Credential.CREATOR); } public static final @NonNull Creator<SaveEntry> CREATOR = new Creator<SaveEntry>() { @@ -65,9 +64,9 @@ public final class SaveEntry implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - mSlice.writeToParcel(dest, flags); - mPendingIntent.writeToParcel(dest, flags); - mCredential.writeToParcel(dest, flags); + dest.writeTypedObject(mSlice, flags); + dest.writeTypedObject(mPendingIntent, flags); + dest.writeTypedObject(mCredential, flags); } /* package-private */ SaveEntry( diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index cb0dce91589e..32bdf7962273 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1047,7 +1047,7 @@ public class DreamService extends Service implements Window.Callback { } if (mDreamToken == null) { - Slog.w(mTag, "Finish was called before the dream was attached."); + if (mDebug) Slog.v(mTag, "finish() called when not attached."); stopSelf(); return; } diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index bd4a4957775e..cfc79e4fef66 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -392,13 +392,13 @@ public abstract class NotificationListenerService extends Service { public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; /** - * An optional activity intent category that shows additional settings for what notifications + * An optional activity intent action that shows additional settings for what notifications * should be processed by this notification listener service. If defined, the OS may link to * this activity from the system notification listener service filter settings page. */ - @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY) - public static final String INTENT_CATEGORY_SETTINGS_HOME = - "android.service.notification.category.SETTINGS_HOME"; + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SETTINGS_HOME = + "android.service.notification.action.SETTINGS_HOME"; private final Object mLock = new Object(); diff --git a/core/java/android/service/timezone/TimeZoneProviderEvent.java b/core/java/android/service/timezone/TimeZoneProviderEvent.java index f6433b7f371e..714afee197f1 100644 --- a/core/java/android/service/timezone/TimeZoneProviderEvent.java +++ b/core/java/android/service/timezone/TimeZoneProviderEvent.java @@ -57,7 +57,7 @@ public final class TimeZoneProviderEvent implements Parcelable { /** * The provider was uncertain about the time zone. See {@link - * TimeZoneProviderService#reportUncertain()} + * TimeZoneProviderService#reportUncertain(TimeZoneProviderStatus)} */ public static final @EventType int EVENT_TYPE_UNCERTAIN = 3; @@ -66,42 +66,55 @@ public final class TimeZoneProviderEvent implements Parcelable { @ElapsedRealtimeLong private final long mCreationElapsedMillis; + // Populated when mType == EVENT_TYPE_SUGGESTION @Nullable private final TimeZoneProviderSuggestion mSuggestion; + // Populated when mType == EVENT_TYPE_PERMANENT_FAILURE @Nullable private final String mFailureCause; - private TimeZoneProviderEvent(@EventType int type, + // Populated when mType == EVENT_TYPE_SUGGESTION or EVENT_TYPE_UNCERTAIN + @Nullable + private final TimeZoneProviderStatus mTimeZoneProviderStatus; + + private TimeZoneProviderEvent(int type, @ElapsedRealtimeLong long creationElapsedMillis, @Nullable TimeZoneProviderSuggestion suggestion, - @Nullable String failureCause) { + @Nullable String failureCause, + @Nullable TimeZoneProviderStatus timeZoneProviderStatus) { mType = type; mCreationElapsedMillis = creationElapsedMillis; mSuggestion = suggestion; mFailureCause = failureCause; + mTimeZoneProviderStatus = timeZoneProviderStatus; } - /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */ + /** Returns an event of type {@link #EVENT_TYPE_SUGGESTION}. */ public static TimeZoneProviderEvent createSuggestionEvent( @ElapsedRealtimeLong long creationElapsedMillis, - @NonNull TimeZoneProviderSuggestion suggestion) { + @NonNull TimeZoneProviderSuggestion suggestion, + @NonNull TimeZoneProviderStatus providerStatus) { return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, creationElapsedMillis, - Objects.requireNonNull(suggestion), null); + Objects.requireNonNull(suggestion), null, Objects.requireNonNull(providerStatus)); } - /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */ + /** Returns an event of type {@link #EVENT_TYPE_UNCERTAIN}. */ public static TimeZoneProviderEvent createUncertainEvent( - @ElapsedRealtimeLong long creationElapsedMillis) { - return new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null); + @ElapsedRealtimeLong long creationElapsedMillis, + @NonNull TimeZoneProviderStatus timeZoneProviderStatus) { + + return new TimeZoneProviderEvent( + EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null, + Objects.requireNonNull(timeZoneProviderStatus)); } - /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */ + /** Returns an event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */ public static TimeZoneProviderEvent createPermanentFailureEvent( @ElapsedRealtimeLong long creationElapsedMillis, @NonNull String cause) { return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, creationElapsedMillis, null, - Objects.requireNonNull(cause)); + Objects.requireNonNull(cause), null); } /** @@ -126,7 +139,7 @@ public final class TimeZoneProviderEvent implements Parcelable { } /** - * Returns the failure cauese. Populated when {@link #getType()} is {@link + * Returns the failure cause. Populated when {@link #getType()} is {@link * #EVENT_TYPE_PERMANENT_FAILURE}. */ @Nullable @@ -134,24 +147,34 @@ public final class TimeZoneProviderEvent implements Parcelable { return mFailureCause; } - public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR = - new Creator<TimeZoneProviderEvent>() { - @Override - public TimeZoneProviderEvent createFromParcel(Parcel in) { - int type = in.readInt(); - long creationElapsedMillis = in.readLong(); - TimeZoneProviderSuggestion suggestion = - in.readParcelable(getClass().getClassLoader(), android.service.timezone.TimeZoneProviderSuggestion.class); - String failureCause = in.readString8(); - return new TimeZoneProviderEvent( - type, creationElapsedMillis, suggestion, failureCause); - } - - @Override - public TimeZoneProviderEvent[] newArray(int size) { - return new TimeZoneProviderEvent[size]; - } - }; + /** + * Returns the status of the time zone provider. Populated when {@link #getType()} is {@link + * #EVENT_TYPE_UNCERTAIN} or {@link #EVENT_TYPE_SUGGESTION}. + */ + @Nullable + public TimeZoneProviderStatus getTimeZoneProviderStatus() { + return mTimeZoneProviderStatus; + } + + public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR = new Creator<>() { + @Override + public TimeZoneProviderEvent createFromParcel(Parcel in) { + int type = in.readInt(); + long creationElapsedMillis = in.readLong(); + TimeZoneProviderSuggestion suggestion = in.readParcelable( + getClass().getClassLoader(), TimeZoneProviderSuggestion.class); + String failureCause = in.readString8(); + TimeZoneProviderStatus status = in.readParcelable( + getClass().getClassLoader(), TimeZoneProviderStatus.class); + return new TimeZoneProviderEvent( + type, creationElapsedMillis, suggestion, failureCause, status); + } + + @Override + public TimeZoneProviderEvent[] newArray(int size) { + return new TimeZoneProviderEvent[size]; + } + }; @Override public int describeContents() { @@ -164,6 +187,7 @@ public final class TimeZoneProviderEvent implements Parcelable { parcel.writeLong(mCreationElapsedMillis); parcel.writeParcelable(mSuggestion, 0); parcel.writeString8(mFailureCause); + parcel.writeParcelable(mTimeZoneProviderStatus, 0); } @Override @@ -173,14 +197,17 @@ public final class TimeZoneProviderEvent implements Parcelable { + ", mCreationElapsedMillis=" + Duration.ofMillis(mCreationElapsedMillis).toString() + ", mSuggestion=" + mSuggestion + ", mFailureCause=" + mFailureCause + + ", mTimeZoneProviderStatus=" + mTimeZoneProviderStatus + '}'; } /** * Similar to {@link #equals} except this methods checks for equivalence, not equality. - * i.e. two {@link #EVENT_TYPE_UNCERTAIN} and {@link #EVENT_TYPE_PERMANENT_FAILURE} events are - * always equivalent, two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest - * the same time zones. + * i.e. two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest + * the same time zones and have the same provider status, two {@link #EVENT_TYPE_UNCERTAIN} + * events are equivalent if they have the same provider status, and {@link + * #EVENT_TYPE_PERMANENT_FAILURE} events are always equivalent (the nature of the failure is not + * considered). */ @SuppressWarnings("ReferenceEquality") public boolean isEquivalentTo(@Nullable TimeZoneProviderEvent other) { @@ -191,9 +218,10 @@ public final class TimeZoneProviderEvent implements Parcelable { return false; } if (mType == EVENT_TYPE_SUGGESTION) { - return mSuggestion.isEquivalentTo(other.getSuggestion()); + return mSuggestion.isEquivalentTo(other.mSuggestion) + && Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus); } - return true; + return Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus); } @Override @@ -208,11 +236,13 @@ public final class TimeZoneProviderEvent implements Parcelable { return mType == that.mType && mCreationElapsedMillis == that.mCreationElapsedMillis && Objects.equals(mSuggestion, that.mSuggestion) - && Objects.equals(mFailureCause, that.mFailureCause); + && Objects.equals(mFailureCause, that.mFailureCause) + && Objects.equals(mTimeZoneProviderStatus, that.mTimeZoneProviderStatus); } @Override public int hashCode() { - return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause); + return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause, + mTimeZoneProviderStatus); } } diff --git a/core/java/android/service/timezone/TimeZoneProviderService.java b/core/java/android/service/timezone/TimeZoneProviderService.java index 0d215f6d56f1..cd4a30598a9b 100644 --- a/core/java/android/service/timezone/TimeZoneProviderService.java +++ b/core/java/android/service/timezone/TimeZoneProviderService.java @@ -203,6 +203,20 @@ public abstract class TimeZoneProviderService extends Service { * details. */ public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion) { + reportSuggestion(suggestion, TimeZoneProviderStatus.UNKNOWN); + } + + /** + * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for + * details. + * + * @param providerStatus provider status information that can influence detector service + * behavior and/or be reported via the device UI + * + * @hide + */ + public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion, + @NonNull TimeZoneProviderStatus providerStatus) { Objects.requireNonNull(suggestion); mHandler.post(() -> { @@ -212,7 +226,7 @@ public abstract class TimeZoneProviderService extends Service { try { TimeZoneProviderEvent thisEvent = TimeZoneProviderEvent.createSuggestionEvent( - SystemClock.elapsedRealtime(), suggestion); + SystemClock.elapsedRealtime(), suggestion, providerStatus); if (shouldSendEvent(thisEvent)) { manager.onTimeZoneProviderEvent(thisEvent); mLastEventSent = thisEvent; @@ -231,6 +245,21 @@ public abstract class TimeZoneProviderService extends Service { * to a time zone. */ public final void reportUncertain() { + reportUncertain(TimeZoneProviderStatus.UNKNOWN); + } + + /** + * Indicates the time zone is not known because of an expected runtime state or error. + * + * <p>When the status changes then a certain or uncertain report must be made to move the + * detector service to the new status. + * + * @param providerStatus provider status information that can influence detector service + * behavior and/or be reported via the device UI + * + * @hide + */ + public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) { mHandler.post(() -> { synchronized (mLock) { ITimeZoneProviderManager manager = mManager; @@ -238,7 +267,7 @@ public abstract class TimeZoneProviderService extends Service { try { TimeZoneProviderEvent thisEvent = TimeZoneProviderEvent.createUncertainEvent( - SystemClock.elapsedRealtime()); + SystemClock.elapsedRealtime(), providerStatus); if (shouldSendEvent(thisEvent)) { manager.onTimeZoneProviderEvent(thisEvent); mLastEventSent = thisEvent; diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.aidl b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl new file mode 100644 index 000000000000..91dc7e99fd9a --- /dev/null +++ b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.timezone; + +/** + * @hide + */ +parcelable TimeZoneProviderStatus; diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.java b/core/java/android/service/timezone/TimeZoneProviderStatus.java new file mode 100644 index 000000000000..87d7843bacaa --- /dev/null +++ b/core/java/android/service/timezone/TimeZoneProviderStatus.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.timezone; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +/** + * Information about the status of a {@link TimeZoneProviderService}. + * + * <p>Not all status properties or status values will apply to all provider implementations. + * {@code _NOT_APPLICABLE} status can be used to indicate properties that have no meaning for a + * given implementation. + * + * <p>Time zone providers are expected to work in one of two ways: + * <ol> + * <li>Location: Providers will determine location and then map that location to one or more + * time zone IDs.</li> + * <li>External signals: Providers could use indirect signals like country code + * and/or local offset / DST information provided to the device to infer a time zone, e.g. + * signals like MCC and NITZ for telephony devices, IP geo location, or DHCP information + * (RFC4833). The time zone ID could also be fed directly to the device by an external service. + * </li> + * </ol> + * + * <p>The status properties are: + * <ul> + * <li>location detection - for location-based providers, the status of the location detection + * mechanism</li> + * <li>connectivity - connectivity can influence providers directly, for example if they use + * a networked service to map location to time zone ID, or use geo IP, or indirectly for + * location detection (e.g. for the network location provider.</li> + * <li>time zone resolution - the status related to determining a time zone ID or using a + * detected time zone ID. For example, a networked service may be reachable (i.e. connectivity + * is working) but the service could return errors, a time zone ID detected may not be usable + * for a device because of TZDB version skew, or external indirect signals may available but + * do not match the properties of a known time zone ID.</li> + * </ul> + * + * @hide + */ +public final class TimeZoneProviderStatus implements Parcelable { + + /** + * A status code related to a dependency a provider may have. + * + * @hide + */ + @IntDef(prefix = "DEPENDENCY_STATUS_", value = { + DEPENDENCY_STATUS_UNKNOWN, + DEPENDENCY_STATUS_NOT_APPLICABLE, + DEPENDENCY_STATUS_WORKING, + DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE, + DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT, + DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS, + DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface DependencyStatus {} + + /** The dependency's status is unknown. */ + public static final @DependencyStatus int DEPENDENCY_STATUS_UNKNOWN = 0; + + /** The dependency is not used by the provider's implementation. */ + public static final @DependencyStatus int DEPENDENCY_STATUS_NOT_APPLICABLE = 1; + + /** The dependency is applicable and working well. */ + public static final @DependencyStatus int DEPENDENCY_STATUS_WORKING = 2; + + /** + * The dependency is used but is temporarily unavailable, e.g. connectivity has been lost for an + * unpredictable amount of time. + * + * <p>This status is considered normal is may be entered many times a day. + */ + public static final @DependencyStatus int DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE = 3; + + /** + * The dependency is used by the provider but is blocked by the environment in a way that the + * provider has detected and is considered likely to persist for some time, e.g. connectivity + * has been lost due to boarding a plane. + * + * <p>This status is considered unusual and could be used by the system as a trigger to try + * other time zone providers / time zone detection mechanisms. The bar for using this status + * should therefore be set fairly high to avoid a device bringing up other providers or + * switching to a different detection mechanism that may provide a different suggestion. + */ + public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = 4; + + /** + * The dependency is used by the provider but is running in a degraded mode due to the user's + * settings. A user can take action to improve this, e.g. by changing a setting. + * + * <p>This status could be used by the system as a trigger to try other time zone + * providers / time zone detection mechanisms. The user may be informed. + */ + public static final @DependencyStatus int DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS = 5; + + /** + * The dependency is used by the provider but is completely blocked by the user's settings. + * A user can take action to correct this, e.g. by changing a setting. + * + * <p>This status could be used by the system as a trigger to try other time zone providers / + * time zone detection mechanisms. The user may be informed. + */ + public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = 6; + + /** + * A status code related to an operation in a provider's detection algorithm. + * + * @hide + */ + @IntDef(prefix = "OPERATION_STATUS_", value = { + OPERATION_STATUS_UNKNOWN, + OPERATION_STATUS_NOT_APPLICABLE, + OPERATION_STATUS_WORKING, + OPERATION_STATUS_FAILED, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface OperationStatus {} + + /** The operation's status is unknown. */ + public static final @OperationStatus int OPERATION_STATUS_UNKNOWN = 0; + + /** The operation is not used by the provider's implementation. */ + public static final @OperationStatus int OPERATION_STATUS_NOT_APPLICABLE = 1; + + /** The operation is applicable and working well. */ + public static final @OperationStatus int OPERATION_STATUS_WORKING = 2; + + /** The operation is applicable and failed. */ + public static final @OperationStatus int OPERATION_STATUS_FAILED = 3; + + /** + * An instance that provides no information about status. Effectively a "null" status. + */ + @NonNull + public static final TimeZoneProviderStatus UNKNOWN = new TimeZoneProviderStatus( + DEPENDENCY_STATUS_UNKNOWN, DEPENDENCY_STATUS_UNKNOWN, OPERATION_STATUS_UNKNOWN); + + private final @DependencyStatus int mLocationDetectionStatus; + private final @DependencyStatus int mConnectivityStatus; + private final @OperationStatus int mTimeZoneResolutionStatus; + + private TimeZoneProviderStatus( + @DependencyStatus int locationDetectionStatus, + @DependencyStatus int connectivityStatus, + @OperationStatus int timeZoneResolutionStatus) { + mLocationDetectionStatus = requireValidDependencyStatus(locationDetectionStatus); + mConnectivityStatus = requireValidDependencyStatus(connectivityStatus); + mTimeZoneResolutionStatus = requireValidOperationStatus(timeZoneResolutionStatus); + } + + /** + * Returns the status of the location detection dependencies used by the provider (where + * applicable). + */ + public @DependencyStatus int getLocationDetectionStatus() { + return mLocationDetectionStatus; + } + + /** + * Returns the status of the connectivity dependencies used by the provider (where applicable). + */ + public @DependencyStatus int getConnectivityStatus() { + return mConnectivityStatus; + } + + /** + * Returns the status of the time zone resolution operation used by the provider. + */ + public @OperationStatus int getTimeZoneResolutionStatus() { + return mTimeZoneResolutionStatus; + } + + @Override + public String toString() { + return "TimeZoneProviderStatus{" + + "mLocationDetectionStatus=" + mLocationDetectionStatus + + ", mConnectivityStatus=" + mConnectivityStatus + + ", mTimeZoneResolutionStatus=" + mTimeZoneResolutionStatus + + '}'; + } + + public static final @NonNull Creator<TimeZoneProviderStatus> CREATOR = new Creator<>() { + @Override + public TimeZoneProviderStatus createFromParcel(Parcel in) { + @DependencyStatus int locationDetectionStatus = in.readInt(); + @DependencyStatus int connectivityStatus = in.readInt(); + @OperationStatus int timeZoneResolutionStatus = in.readInt(); + return new TimeZoneProviderStatus( + locationDetectionStatus, connectivityStatus, timeZoneResolutionStatus); + } + + @Override + public TimeZoneProviderStatus[] newArray(int size) { + return new TimeZoneProviderStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mLocationDetectionStatus); + parcel.writeInt(mConnectivityStatus); + parcel.writeInt(mTimeZoneResolutionStatus); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneProviderStatus that = (TimeZoneProviderStatus) o; + return mLocationDetectionStatus == that.mLocationDetectionStatus + && mConnectivityStatus == that.mConnectivityStatus + && mTimeZoneResolutionStatus == that.mTimeZoneResolutionStatus; + } + + @Override + public int hashCode() { + return Objects.hash( + mLocationDetectionStatus, mConnectivityStatus, mTimeZoneResolutionStatus); + } + + /** A builder for {@link TimeZoneProviderStatus}. */ + public static final class Builder { + + private @DependencyStatus int mLocationDetectionStatus = DEPENDENCY_STATUS_UNKNOWN; + private @DependencyStatus int mConnectivityStatus = DEPENDENCY_STATUS_UNKNOWN; + private @OperationStatus int mTimeZoneResolutionStatus = OPERATION_STATUS_UNKNOWN; + + /** + * Creates a new builder instance. At creation time all status properties are set to + * their "UNKNOWN" value. + */ + public Builder() { + } + + /** + * @hide + */ + public Builder(TimeZoneProviderStatus toCopy) { + mLocationDetectionStatus = toCopy.mLocationDetectionStatus; + mConnectivityStatus = toCopy.mConnectivityStatus; + mTimeZoneResolutionStatus = toCopy.mTimeZoneResolutionStatus; + } + + /** + * Sets the status of the provider's location detection dependency (where applicable). + * See the {@code DEPENDENCY_STATUS_} constants for more information. + */ + @NonNull + public Builder setLocationDetectionStatus(@DependencyStatus int locationDetectionStatus) { + mLocationDetectionStatus = locationDetectionStatus; + return this; + } + + /** + * Sets the status of the provider's connectivity dependency (where applicable). + * See the {@code DEPENDENCY_STATUS_} constants for more information. + */ + @NonNull + public Builder setConnectivityStatus(@DependencyStatus int connectivityStatus) { + mConnectivityStatus = connectivityStatus; + return this; + } + + /** + * Sets the status of the provider's time zone resolution operation. + * See the {@code OPERATION_STATUS_} constants for more information. + */ + @NonNull + public Builder setTimeZoneResolutionStatus(@OperationStatus int timeZoneResolutionStatus) { + mTimeZoneResolutionStatus = timeZoneResolutionStatus; + return this; + } + + /** + * Builds a {@link TimeZoneProviderStatus} instance. + */ + @NonNull + public TimeZoneProviderStatus build() { + return new TimeZoneProviderStatus( + mLocationDetectionStatus, mConnectivityStatus, mTimeZoneResolutionStatus); + } + } + + private @OperationStatus int requireValidOperationStatus(@OperationStatus int operationStatus) { + if (operationStatus < OPERATION_STATUS_UNKNOWN + || operationStatus > OPERATION_STATUS_FAILED) { + throw new IllegalArgumentException(Integer.toString(operationStatus)); + } + return operationStatus; + } + + private static @DependencyStatus int requireValidDependencyStatus( + @DependencyStatus int dependencyStatus) { + if (dependencyStatus < DEPENDENCY_STATUS_UNKNOWN + || dependencyStatus > DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS) { + throw new IllegalArgumentException(Integer.toString(dependencyStatus)); + } + return dependencyStatus; + } +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 519fc55b523d..1337d6a87df8 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -36,7 +36,6 @@ import android.text.style.LineBackgroundSpan; import android.text.style.ParagraphStyle; import android.text.style.ReplacementSpan; import android.text.style.TabStopSpan; -import android.util.Range; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -1859,13 +1858,12 @@ public abstract class Layout { * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a * text segment * @param inclusionStrategy strategy for determining whether a text segment is inside the - * specified area - * @return an integer range where the endpoints are the start (inclusive) and end (exclusive) - * character offsets of the text range, or null if there are no text segments inside the - * area + * specified area + * @return int array of size 2 containing the start (inclusive) and end (exclusive) character + * offsets of the text range, or null if there are no text segments inside the area */ @Nullable - public Range<Integer> getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, + public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy) { // Find the first line whose bottom (without line spacing) is below the top of the area. int startLine = getLineForVertical((int) area.top); @@ -1923,7 +1921,7 @@ public abstract class Layout { start = segmentFinder.previousStartBoundary(start + 1); end = segmentFinder.nextEndBoundary(end - 1); - return new Range(start, end); + return new int[] {start, end}; } /** diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 9789b5670fbb..06c1b258cb70 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -2001,7 +2001,6 @@ public class KeyEvent extends InputEvent implements Parcelable { case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index bfa13507ed50..efda257aed27 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -888,20 +888,18 @@ public final class ViewRootImpl implements ViewParent, static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback = new BLASTBufferQueue.TransactionHangCallback() { @Override - public void onTransactionHang(boolean isGPUHang) { - if (isGPUHang && !sAnrReported) { - sAnrReported = true; - try { - ActivityManager.getService().appNotResponding( - "Buffer processing hung up due to stuck fence. Indicates GPU hang"); - } catch (RemoteException e) { - // We asked the system to crash us, but the system - // already crashed. Unfortunately things may be - // out of control. - } - } else { - // TODO: Do something with this later. For now we just ANR - // in dequeue buffer later like we always have. + public void onTransactionHang(String reason) { + if (sAnrReported) { + return; + } + + sAnrReported = true; + try { + ActivityManager.getService().appNotResponding(reason); + } catch (RemoteException e) { + // We asked the system to crash us, but the system + // already crashed. Unfortunately things may be + // out of control. } } }; diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 201efe81c102..69eed0abb1a6 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -30,7 +30,9 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static android.view.inputmethod.InputMethodManagerProto.ACTIVE; import static android.view.inputmethod.InputMethodManagerProto.CUR_ID; import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE; +import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW; import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING; +import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW; import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS; @@ -763,13 +765,11 @@ public final class InputMethodManager { forceFocus = true; } } - startInputOnWindowFocusGain(viewForWindowFocus, - windowAttribute.softInputMode, windowAttribute.flags, forceFocus); - } - private void startInputOnWindowFocusGain(View focusedView, - @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) { - int startInputFlags = getStartInputFlags(focusedView, 0); + final int softInputMode = windowAttribute.softInputMode; + final int windowFlags = windowAttribute.flags; + + int startInputFlags = getStartInputFlags(viewForWindowFocus, 0); startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS; ImeTracing.getInstance().triggerClientDump( @@ -784,9 +784,9 @@ public final class InputMethodManager { if (mRestartOnNextWindowFocus) { if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true"); mRestartOnNextWindowFocus = false; - forceNewFocus = true; + forceFocus = true; } - checkFocusResult = checkFocusInternalLocked(forceNewFocus, mCurRootView); + checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView); } if (checkFocusResult) { @@ -795,7 +795,7 @@ public final class InputMethodManager { // about the window gaining focus, to help make the transition // smooth. if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN, - focusedView, startInputFlags, softInputMode, windowFlags)) { + viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) { return; } } @@ -810,7 +810,7 @@ public final class InputMethodManager { // ignore the result mServiceInvoker.startInputOrWindowGainedFocus( StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, - focusedView.getWindowToken(), startInputFlags, softInputMode, + viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode, windowFlags, null, null, null, @@ -903,8 +903,6 @@ public final class InputMethodManager { /** * Checks whether the active input connection (if any) is for the given view. * - * TODO(b/182259171): Clean-up hasActiveConnection to simplify the logic. - * * Note that this method is only intended for restarting input after focus gain * (e.g. b/160391516), DO NOT leverage this method to do another check. */ @@ -915,7 +913,6 @@ public final class InputMethodManager { } return mServedInputConnection != null - && mServedInputConnection.isActive() && mServedInputConnection.isAssociatedWith(view); } } @@ -1128,10 +1125,13 @@ public final class InputMethodManager { if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) { return; } - final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS - : StartInputReason.DEACTIVATED_BY_IMMS; - startInputOnWindowFocusGainInternal(reason, null, 0, 0, 0); + mCurrentEditorInfo = null; + mCompletions = null; + mServedConnecting = true; } + final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS + : StartInputReason.DEACTIVATED_BY_IMMS; + startInputInner(reason, null, 0, 0, 0); return; } case MSG_SET_INTERACTIVE: { @@ -3992,6 +3992,8 @@ public final class InputMethodManager { proto.write(FULLSCREEN_MODE, mFullscreenMode); proto.write(ACTIVE, mActive); proto.write(SERVED_CONNECTING, mServedConnecting); + proto.write(SERVED_VIEW, Objects.toString(mServedView)); + proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView)); proto.end(token); if (mCurRootView != null) { mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL); diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java index fa18eeceafd0..f2b70997de63 100644 --- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java @@ -214,7 +214,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { } } - public boolean isActive() { + private boolean isActive() { return mParentInputMethodManager.isActive() && !isFinished(); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 57103e4955ca..b5c58fb4bfc0 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -151,7 +151,6 @@ import android.util.DisplayMetrics; import android.util.FeatureFlagUtils; import android.util.IntArray; import android.util.Log; -import android.util.Range; import android.util.SparseIntArray; import android.util.TypedValue; import android.view.AccessibilityIterators.TextSegmentIterator; @@ -9317,40 +9316,42 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) { - Range<Integer> range = getRangeForRect( + int[] range = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getSelectionArea()), gesture.getGranularity()); if (range == null) { return handleGestureFailure(gesture); } - Selection.setSelection(getEditableText(), range.getLower(), range.getUpper()); + Selection.setSelection(getEditableText(), range[0], range[1]); mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } /** @hide */ public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) { - Range<Integer> startRange = getRangeForRect( + int[] startRange = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()), gesture.getGranularity()); if (startRange == null) { return handleGestureFailure(gesture); } - Range<Integer> endRange = getRangeForRect( + int[] endRange = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()), gesture.getGranularity()); - if (endRange == null || endRange.getUpper() <= startRange.getLower()) { + if (endRange == null) { return handleGestureFailure(gesture); } - Range<Integer> range = startRange.extend(endRange); - Selection.setSelection(getEditableText(), range.getLower(), range.getUpper()); + int[] range = new int[] { + Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1]) + }; + Selection.setSelection(getEditableText(), range[0], range[1]); mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } /** @hide */ public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) { - Range<Integer> range = getRangeForRect( + int[] range = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getDeletionArea()), gesture.getGranularity()); if (range == null) { @@ -9361,42 +9362,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener range = adjustHandwritingDeleteGestureRange(range); } - getEditableText().delete(range.getLower(), range.getUpper()); - Selection.setSelection(getEditableText(), range.getLower()); + getEditableText().delete(range[0], range[1]); + Selection.setSelection(getEditableText(), range[0]); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } /** @hide */ public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) { - Range<Integer> startRange = getRangeForRect( + int[] startRange = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()), gesture.getGranularity()); if (startRange == null) { return handleGestureFailure(gesture); } - Range<Integer> endRange = getRangeForRect( + int[] endRange = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()), gesture.getGranularity()); if (endRange == null) { return handleGestureFailure(gesture); } - Range<Integer> range = startRange.extend(endRange); + int[] range = new int[] { + Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1]) + }; if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) { range = adjustHandwritingDeleteGestureRange(range); } - getEditableText().delete(range.getLower(), range.getUpper()); - Selection.setSelection(getEditableText(), range.getLower()); + getEditableText().delete(range[0], range[1]); + Selection.setSelection(getEditableText(), range[0]); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } - private Range<Integer> adjustHandwritingDeleteGestureRange(Range<Integer> range) { + private int[] adjustHandwritingDeleteGestureRange(int[] range) { // For handwriting delete gestures with word granularity, adjust the start and end offsets // to remove extra whitespace around the deleted text. - int start = range.getLower(); - int end = range.getUpper(); + int start = range[0]; + int end = range[1]; // If the deleted text is at the start of the text, the behavior is the same as the case // where the deleted text follows a new line character. @@ -9425,7 +9428,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (start == 0) break; codePointBeforeStart = Character.codePointBefore(mText, start); } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)); - return new Range(start, end); + return new int[] {start, end}; } if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd) @@ -9444,7 +9447,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (end == mText.length()) break; codePointAtEnd = Character.codePointAt(mText, end); } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)); - return new Range(start, end); + return new int[] {start, end}; } // Return the original range. @@ -9494,14 +9497,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener lineVerticalCenter + 0.1f, Math.max(startPoint.x, endPoint.x), lineVerticalCenter - 0.1f); - Range<Integer> range = mLayout.getRangeForRect( + int[] range = mLayout.getRangeForRect( area, new GraphemeClusterSegmentFinder(mText, mTextPaint), Layout.INCLUSION_STRATEGY_ANY_OVERLAP); if (range == null) { return handleGestureFailure(gesture); } - int startOffset = range.getLower(); - int endOffset = range.getUpper(); + int startOffset = range[0]; + int endOffset = range[1]; // TODO(b/247557062): This doesn't handle bidirectional text correctly. Pattern whitespacePattern = getWhitespacePattern(); @@ -9606,7 +9609,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Nullable - private Range<Integer> getRangeForRect(@NonNull RectF area, int granularity) { + private int[] getRangeForRect(@NonNull RectF area, int granularity) { SegmentFinder segmentFinder; if (granularity == HandwritingGesture.GRANULARITY_WORD) { WordIterator wordIterator = getWordIterator(); diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java index 4a4f561c71ed..85b288113b45 100644 --- a/core/java/android/window/BackEvent.java +++ b/core/java/android/window/BackEvent.java @@ -18,8 +18,10 @@ package android.window; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import android.view.RemoteAnimationTarget; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -50,6 +52,8 @@ public class BackEvent implements Parcelable { @SwipeEdge private final int mSwipeEdge; + @Nullable + private final RemoteAnimationTarget mDepartingAnimationTarget; /** * Creates a new {@link BackEvent} instance. @@ -58,12 +62,16 @@ public class BackEvent implements Parcelable { * @param touchY Absolute Y location of the touch point of this event. * @param progress Value between 0 and 1 on how far along the back gesture is. * @param swipeEdge Indicates which edge the swipe starts from. + * @param departingAnimationTarget The remote animation target of the departing + * application window. */ - public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) { + public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge, + @Nullable RemoteAnimationTarget departingAnimationTarget) { mTouchX = touchX; mTouchY = touchY; mProgress = progress; mSwipeEdge = swipeEdge; + mDepartingAnimationTarget = departingAnimationTarget; } private BackEvent(@NonNull Parcel in) { @@ -71,6 +79,7 @@ public class BackEvent implements Parcelable { mTouchY = in.readFloat(); mProgress = in.readFloat(); mSwipeEdge = in.readInt(); + mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR); } public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() { @@ -96,6 +105,7 @@ public class BackEvent implements Parcelable { dest.writeFloat(mTouchY); dest.writeFloat(mProgress); dest.writeInt(mSwipeEdge); + dest.writeTypedObject(mDepartingAnimationTarget, flags); } /** @@ -126,6 +136,16 @@ public class BackEvent implements Parcelable { return mSwipeEdge; } + /** + * Returns the {@link RemoteAnimationTarget} of the top departing application window, + * or {@code null} if the top window should not be moved for the current type of back + * destination. + */ + @Nullable + public RemoteAnimationTarget getDepartingAnimationTarget() { + return mDepartingAnimationTarget; + } + @Override public String toString() { return "BackEvent{" diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java new file mode 100644 index 000000000000..2e3afde1a78a --- /dev/null +++ b/core/java/android/window/BackProgressAnimator.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.util.FloatProperty; + +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; + +/** + * An animator that drives the predictive back progress with a spring. + * + * The back gesture's latest touch point and committal state determines the final position of + * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with + * smoothly transitioning progress values. + * + * @hide + */ +public class BackProgressAnimator { + /** + * A factor to scale the input progress by, so that it works better with the spring. + * We divide the output progress by this value before sending it to apps, so that apps + * always receive progress values in [0, 1]. + */ + private static final float SCALE_FACTOR = 100f; + private final SpringAnimation mSpring; + private ProgressCallback mCallback; + private float mProgress = 0; + private BackEvent mLastBackEvent; + private boolean mStarted = false; + + private void setProgress(float progress) { + mProgress = progress; + } + + private float getProgress() { + return mProgress; + } + + private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP = + new FloatProperty<BackProgressAnimator>("progress") { + @Override + public void setValue(BackProgressAnimator animator, float value) { + animator.setProgress(value); + animator.updateProgressValue(value); + } + + @Override + public Float get(BackProgressAnimator object) { + return object.getProgress(); + } + }; + + + /** A callback to be invoked when there's a progress value update from the animator. */ + public interface ProgressCallback { + /** Called when there's a progress value update. */ + void onProgressUpdate(BackEvent event); + } + + public BackProgressAnimator() { + mSpring = new SpringAnimation(this, PROGRESS_PROP); + mSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + } + + /** + * Sets a new target position for the back progress. + * + * @param event the {@link BackEvent} containing the latest target progress. + */ + public void onBackProgressed(BackEvent event) { + if (!mStarted) { + return; + } + mLastBackEvent = event; + if (mSpring == null) { + return; + } + mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR); + } + + /** + * Starts the back progress animation. + * + * @param event the {@link BackEvent} that started the gesture. + * @param callback the back callback to invoke for the gesture. It will receive back progress + * dispatches as the progress animation updates. + */ + public void onBackStarted(BackEvent event, ProgressCallback callback) { + reset(); + mLastBackEvent = event; + mCallback = callback; + mStarted = true; + } + + /** + * Resets the back progress animation. This should be called when back is invoked or cancelled. + */ + public void reset() { + mSpring.animateToFinalPosition(0); + if (mSpring.canSkipToEnd()) { + mSpring.skipToEnd(); + } else { + // Should never happen. + mSpring.cancel(); + } + mStarted = false; + mLastBackEvent = null; + mCallback = null; + mProgress = 0; + } + + private void updateProgressValue(float progress) { + if (mLastBackEvent == null || mCallback == null || !mStarted) { + return; + } + mCallback.onProgressUpdate( + new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(), + progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(), + mLastBackEvent.getDepartingAnimationTarget())); + } + +} diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl index 47796de11dd5..6af8ddda3a62 100644 --- a/core/java/android/window/IOnBackInvokedCallback.aidl +++ b/core/java/android/window/IOnBackInvokedCallback.aidl @@ -28,17 +28,18 @@ import android.window.BackEvent; oneway interface IOnBackInvokedCallback { /** * Called when a back gesture has been started, or back button has been pressed down. - * Wraps {@link OnBackInvokedCallback#onBackStarted()}. + * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}. + * + * @param backEvent The {@link BackEvent} containing information about the touch or button press. */ - void onBackStarted(); + void onBackStarted(in BackEvent backEvent); /** * Called on back gesture progress. - * Wraps {@link OnBackInvokedCallback#onBackProgressed()}. + * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}. * - * @param touchX Absolute X location of the touch point. - * @param touchY Absolute Y location of the touch point. - * @param progress Value between 0 and 1 on how far along the back gesture is. + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. */ void onBackProgressed(in BackEvent backEvent); diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java index 1a37e57df403..c05809bca4a5 100644 --- a/core/java/android/window/OnBackAnimationCallback.java +++ b/core/java/android/window/OnBackAnimationCallback.java @@ -13,14 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.window; - import android.annotation.NonNull; import android.app.Activity; import android.app.Dialog; import android.view.View; - /** * Interface for applications to register back animation callbacks along their custom back * handling. @@ -40,11 +37,10 @@ import android.view.View; * @hide */ public interface OnBackAnimationCallback extends OnBackInvokedCallback { - /** - * Called when a back gesture has been started, or back button has been pressed down. - */ + /** + * Called when a back gesture has been started, or back button has been pressed down. + */ default void onBackStarted() { } - /** * Called on back gesture progress. * @@ -53,7 +49,6 @@ public interface OnBackAnimationCallback extends OnBackInvokedCallback { * @see BackEvent */ default void onBackProgressed(@NonNull BackEvent backEvent) { } - /** * Called when a back gesture or back button press has been cancelled. */ diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java index 6e2d4f9edbc1..62c41bfb0681 100644 --- a/core/java/android/window/OnBackInvokedCallback.java +++ b/core/java/android/window/OnBackInvokedCallback.java @@ -16,6 +16,7 @@ package android.window; +import android.annotation.NonNull; import android.app.Activity; import android.app.Dialog; import android.view.Window; @@ -41,8 +42,35 @@ import android.view.Window; @SuppressWarnings("deprecation") public interface OnBackInvokedCallback { /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent The {@link BackEvent} containing information about the touch or + * button press. + * + * @hide + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} + + /** + * Called when a back gesture has been progressed. + * + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. + * + * @hide + */ + default void onBackProgressed(@NonNull BackEvent backEvent) {} + + /** * Called when a back gesture has been completed and committed, or back button pressed * has been released and committed. */ void onBackInvoked(); + + /** + * Called when a back gesture or button press has been cancelled. + * + * @hide + */ + default void onBackCancelled() {} } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 0730f3ddf8ac..fda39c14dac7 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -218,19 +218,24 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { public Checker getChecker() { return mChecker; } + @NonNull + private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { private final WeakReference<OnBackInvokedCallback> mCallback; + OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { mCallback = new WeakReference<>(callback); } @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackStarted(); + mProgressAnimator.onBackStarted(backEvent, event -> + callback.onBackProgressed(event)); + callback.onBackStarted(backEvent); } }); } @@ -240,7 +245,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackProgressed(backEvent); + mProgressAnimator.onBackProgressed(backEvent); } }); } @@ -248,6 +253,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackCancelled() { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { callback.onBackCancelled(); @@ -258,6 +264,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallback.get(); if (callback == null) { return; diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 4a1f7eb06c40..42b46cda6ba3 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -647,15 +647,16 @@ public class ResolverListAdapter extends BaseAdapter { if (info instanceof DisplayResolveInfo) { DisplayResolveInfo dri = (DisplayResolveInfo) info; - boolean hasLabel = dri.hasDisplayLabel(); - holder.bindLabel( - dri.getDisplayLabel(), - dri.getExtendedInfo(), - hasLabel && alwaysShowSubLabel()); - holder.bindIcon(info); - if (!hasLabel) { + if (dri.hasDisplayLabel()) { + holder.bindLabel( + dri.getDisplayLabel(), + dri.getExtendedInfo(), + alwaysShowSubLabel()); + } else { + holder.bindLabel("", "", false); loadLabel(dri); } + holder.bindIcon(info); if (!dri.hasDisplayIcon()) { loadIcon(dri); } diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index e74898294c09..8e7207fa91ee 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -45,6 +45,7 @@ interface IAppWidgetService { @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId); int[] getAppWidgetIdsForHost(String callingPackage, int hostId); + void setAppWidgetHidden(in String callingPackage, int hostId); IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId, int intentFlags); diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 76f33a6c3937..b0d59226d4ec 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -45,6 +45,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL; @@ -224,6 +225,7 @@ public class InteractionJankMonitor { public static final int CUJ_SHADE_CLEAR_ALL = 62; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63; public static final int CUJ_LOCKSCREEN_OCCLUSION = 64; + public static final int CUJ_RECENTS_SCROLLING = 65; private static final int NO_STATSD_LOGGING = -1; @@ -297,6 +299,7 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING, }; private static class InstanceHolder { @@ -385,7 +388,8 @@ public class InteractionJankMonitor { CUJ_TASKBAR_COLLAPSE, CUJ_SHADE_CLEAR_ALL, CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, - CUJ_LOCKSCREEN_OCCLUSION + CUJ_LOCKSCREEN_OCCLUSION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -900,6 +904,8 @@ public class InteractionJankMonitor { return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION"; case CUJ_LOCKSCREEN_OCCLUSION: return "LOCKSCREEN_OCCLUSION"; + case CUJ_RECENTS_SCROLLING: + return "RECENTS_SCROLLING"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java index a09c8236b47d..04dd2d72729d 100644 --- a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java +++ b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java @@ -97,7 +97,6 @@ public class PhoneFallbackEventHandler implements FallbackEventHandler { case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: @@ -224,7 +223,6 @@ public class PhoneFallbackEventHandler implements FallbackEventHandler { } case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java index 295dc545ef4b..25ac1bd678c6 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -41,12 +41,16 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorSpace; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.ImageReader; import android.os.SystemProperties; import android.util.Slog; +import android.view.SurfaceControl; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.TransitionOldType; import android.view.WindowManager.TransitionType; @@ -59,9 +63,11 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; +import android.window.ScreenCapture; import com.android.internal.R; +import java.nio.ByteBuffer; import java.util.List; /** @hide */ @@ -1262,4 +1268,90 @@ public class TransitionAnimation { return set; } + + /** Returns whether the hardware buffer passed in is marked as protected. */ + public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { + return (hardwareBuffer.getUsage() & HardwareBuffer.USAGE_PROTECTED_CONTENT) + == HardwareBuffer.USAGE_PROTECTED_CONTENT; + } + + /** Returns the luminance in 0~1. */ + public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) { + final ScreenCapture.ScreenshotHardwareBuffer buffer = + ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1); + if (buffer != null) { + return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); + } + return 0; + } + + /** Returns the luminance in 0~1. */ + public static float getBorderLuma(HardwareBuffer hwBuffer, ColorSpace colorSpace) { + if (hwBuffer == null) { + return 0; + } + final int format = hwBuffer.getFormat(); + // Only support RGB format in 4 bytes. And protected buffer is not readable. + if (format != HardwareBuffer.RGBA_8888 || hasProtectedContent(hwBuffer)) { + return 0; + } + + final ImageReader ir = ImageReader.newInstance(hwBuffer.getWidth(), hwBuffer.getHeight(), + format, 1 /* maxImages */); + ir.getSurface().attachAndQueueBufferWithColorSpace(hwBuffer, colorSpace); + final Image image = ir.acquireLatestImage(); + if (image == null || image.getPlaneCount() < 1) { + return 0; + } + + final Image.Plane plane = image.getPlanes()[0]; + final ByteBuffer buffer = plane.getBuffer(); + final int width = image.getWidth(); + final int height = image.getHeight(); + final int pixelStride = plane.getPixelStride(); + final int rowStride = plane.getRowStride(); + final int sampling = 10; + final int[] borderLumas = new int[(width + height) * 2 / sampling]; + + // Grab the top and bottom borders. + int i = 0; + for (int x = 0, size = width - sampling; x < size; x += sampling) { + borderLumas[i++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); + borderLumas[i++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); + } + + // Grab the left and right borders. + for (int y = 0, size = height - sampling; y < size; y += sampling) { + borderLumas[i++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); + borderLumas[i++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); + } + + ir.close(); + + // Get "mode" by histogram. + final int[] histogram = new int[256]; + int maxCount = 0; + int mostLuma = 0; + for (int luma : borderLumas) { + final int count = ++histogram[luma]; + if (count > maxCount) { + maxCount = count; + mostLuma = luma; + } + } + return mostLuma / 255f; + } + + /** Returns the luminance of the pixel in 0~255. */ + private static int getPixelLuminance(ByteBuffer buffer, int x, int y, int pixelStride, + int rowStride) { + final int color = buffer.getInt(y * rowStride + x * pixelStride); + // The buffer from ImageReader is always in native order (little-endian), so extract the + // color components in reversed order. + final int r = color & 0xff; + final int g = (color >> 8) & 0xff; + final int b = (color >> 16) & 0xff; + // Approximation of WCAG 2.0 relative luminance. + return ((r * 8) + (g * 22) + (b * 2)) >> 5; + } } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 44cfe1aa4a79..1d4b246de5c8 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -322,4 +322,7 @@ oneway interface IStatusBar /** Unregisters a nearby media devices provider. */ void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); + + /** Dump protos from SystemUI. The proto definition is defined there */ + void dumpProto(in String[] args, in ParcelFileDescriptor pfd); } diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index 1520ea5c6831..03815108f6dd 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -71,10 +71,12 @@ public: } } - void onTransactionHang(bool isGpuHang) { + void onTransactionHang(const std::string& reason) { if (mTransactionHangObject) { + JNIEnv* env = getenv(mVm); + ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str())); getenv(mVm)->CallVoidMethod(mTransactionHangObject, - gTransactionHangCallback.onTransactionHang, isGpuHang); + gTransactionHangCallback.onTransactionHang, jReason.get()); } } @@ -177,7 +179,7 @@ static bool nativeIsSameSurfaceControl(JNIEnv* env, jclass clazz, jlong ptr, jlo sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr); return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl)); } - + static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr, jobject transactionHangCallback) { sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr); @@ -186,9 +188,8 @@ static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong pt } else { sp<TransactionHangCallbackWrapper> wrapper = new TransactionHangCallbackWrapper{env, transactionHangCallback}; - queue->setTransactionHangCallback([wrapper](bool isGpuHang) { - wrapper->onTransactionHang(isGpuHang); - }); + queue->setTransactionHangCallback( + [wrapper](const std::string& reason) { wrapper->onTransactionHang(reason); }); } } @@ -236,7 +237,8 @@ int register_android_graphics_BLASTBufferQueue(JNIEnv* env) { jclass transactionHangClass = FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback"); gTransactionHangCallback.onTransactionHang = - GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V"); + GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", + "(Ljava/lang/String;)V"); return 0; } diff --git a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp index 09f3a727d16e..2437a511238c 100644 --- a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp +++ b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp @@ -89,42 +89,39 @@ static sp<Surface> getSurface(JNIEnv* env, jobject surface) { extern "C" { -static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) { - ALOGV("nativeDetectSurfaceDataspace"); +static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) { + ALOGV("nativeDetectSurfaceType"); sp<ANativeWindow> anw; if ((anw = getNativeWindow(env, surface)) == NULL) { ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__); return BAD_VALUE; } int32_t fmt = 0; - status_t err = anw->query(anw.get(), NATIVE_WINDOW_DEFAULT_DATASPACE, &fmt); + status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &fmt); if (err != NO_ERROR) { - ALOGE("%s: Error while querying surface dataspace %s (%d).", __FUNCTION__, strerror(-err), - err); + ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__, + strerror(-err), err); OVERRIDE_SURFACE_ERROR(err); return err; } return fmt; } -static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) { - ALOGV("nativeDetectSurfaceType"); +static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) { + ALOGV("nativeDetectSurfaceDataspace"); sp<ANativeWindow> anw; if ((anw = getNativeWindow(env, surface)) == NULL) { ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__); return BAD_VALUE; } - int32_t halFmt = 0; - status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &halFmt); + int32_t fmt = 0; + status_t err = anw->query(anw.get(), NATIVE_WINDOW_DEFAULT_DATASPACE, &fmt); if (err != NO_ERROR) { - ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__, - strerror(-err), err); + ALOGE("%s: Error while querying surface dataspace %s (%d).", __FUNCTION__, strerror(-err), + err); OVERRIDE_SURFACE_ERROR(err); return err; } - int32_t dataspace = SurfaceUtils_nativeDetectSurfaceDataspace(env, thiz, surface); - int32_t fmt = static_cast<int32_t>( - mapHalFormatDataspaceToPublicFormat(halFmt, static_cast<android_dataspace>(dataspace))); return fmt; } diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index 40f6e4f63cd7..5c71f692b80f 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -580,6 +580,7 @@ void FileDescriptorTable::RestatInternal(std::set<int>& open_fds, fail_fn_t fail // TODO(narayan): This will be an error in a future android release. // error = true; // ALOGW("Zygote closed file descriptor %d.", it->first); + delete it->second; it = open_fd_map_.erase(it); } else { // The entry from the file descriptor table is still open. Restat diff --git a/core/proto/android/view/imefocuscontroller.proto b/core/proto/android/view/imefocuscontroller.proto index ff9dee69207b..ccde9b7a7966 100644 --- a/core/proto/android/view/imefocuscontroller.proto +++ b/core/proto/android/view/imefocuscontroller.proto @@ -25,6 +25,6 @@ option java_multiple_files = true; */ message ImeFocusControllerProto { optional bool has_ime_focus = 1; - optional string served_view = 2; - optional string next_served_view = 3; + optional string served_view = 2 [deprecated = true]; + optional string next_served_view = 3 [deprecated = true]; }
\ No newline at end of file diff --git a/core/proto/android/view/inputmethod/inputmethodmanager.proto b/core/proto/android/view/inputmethod/inputmethodmanager.proto index 9fed0ef95a27..ea5f1e8f3be2 100644 --- a/core/proto/android/view/inputmethod/inputmethodmanager.proto +++ b/core/proto/android/view/inputmethod/inputmethodmanager.proto @@ -29,4 +29,6 @@ message InputMethodManagerProto { optional int32 display_id = 3; optional bool active = 4; optional bool served_connecting = 5; + optional string served_view = 6; + optional string next_served_view = 7; }
\ No newline at end of file diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6ce31fc13f85..554b15374943 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1151,7 +1151,28 @@ android:protectionLevel="dangerous" /> <!-- Allows an application to write to external storage. - <p class="note"><strong>Note:</strong> If <em>both</em> your <a + <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or + higher, this permission has no effect. + + <p>If your app is on a device that runs API level 19 or higher, you don't need to declare + this permission to read and write files in your application-specific directories returned + by {@link android.content.Context#getExternalFilesDir} and + {@link android.content.Context#getExternalCacheDir}. + + <p>Learn more about how to + <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media + files</a> that your app doesn't own, and how to + <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a> + that your app doesn't own. + + <p>If your app is a file manager and needs broad access to external storage files, then + the system must place your app on an allowlist so that you can successfully request the + <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission. + Learn more about the appropriate use cases for + <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage + device</a>. + + <p>If <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a> and <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code @@ -1159,12 +1180,6 @@ grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> is 4 or higher. - <p>Starting in API level 19, this permission is <em>not</em> required to - read/write files in your application-specific directories returned by - {@link android.content.Context#getExternalFilesDir} and - {@link android.content.Context#getExternalCacheDir}. - <p>If this permission is not allowlisted for an app that targets an API level before - {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p> <p>Protection level: dangerous</p> --> <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" @@ -3042,6 +3057,12 @@ <permission android:name="android.permission.CREATE_USERS" android:protectionLevel="signature" /> + <!-- @SystemApi @hide Allows an application to set user association + with a certain subscription. Used by Enterprise to associate a + subscription with a work or personal profile. --> + <permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" + android:protectionLevel="signature" /> + <!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the device. --> <permission android:name="android.permission.QUERY_USERS" @@ -3155,6 +3176,13 @@ <!-- Allows an application to call {@link android.app.ActivityManager#killBackgroundProcesses}. + <p>As of Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + the {@link android.app.ActivityManager#killBackgroundProcesses} is no longer available to + third party applications. For backwards compatibility, the background processes of the + caller's own package will still be killed when calling this API. If the caller has + the system permission {@code KILL_ALL_BACKGROUND_PROCESSES}, other processes will be + killed too. + <p>Protection level: normal --> <permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" @@ -3162,6 +3190,16 @@ android:description="@string/permdesc_killBackgroundProcesses" android:protectionLevel="normal" /> + <!-- @SystemApi @hide Allows an application to call + {@link android.app.ActivityManager#killBackgroundProcesses} + to kill background processes of other apps. + <p>Not for use by third-party applications. + --> + <permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" + android:label="@string/permlab_killBackgroundProcesses" + android:description="@string/permdesc_killBackgroundProcesses" + android:protectionLevel="signature|privileged" /> + <!-- @SystemApi @hide Allows an application to query process states and current OOM adjustment scores. <p>Not for use by third-party applications. --> @@ -4260,6 +4298,13 @@ <permission android:name="android.permission.BIND_AUTOFILL_SERVICE" android:protectionLevel="signature" /> + <!-- Must be required by a CredentialProviderService to ensure that only the + system can bind to it. + <p>Protection level: signature + --> + <permission android:name="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE" + android:protectionLevel="signature" /> + <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE. This permission was renamed during the O previews but it was supported on the final O release, so we need to carry it over. @@ -6570,6 +6615,13 @@ android:protectionLevel="signature" /> <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" /> + <!-- Allows financed device kiosk apps to perform actions on the Device Lock service + <p>Protection level: internal|role + <p>Intended for use by the FINANCED_DEVICE_KIOSK role only. + --> + <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" + android:protectionLevel="internal|role" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml index c4599dad31a0..8df624fdd2e5 100644 --- a/core/res/res/anim/dream_activity_close_exit.xml +++ b/core/res/res/anim/dream_activity_close_exit.xml @@ -19,5 +19,5 @@ <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="0.0" - android:duration="100" /> + android:duration="@integer/config_dreamCloseAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml index 9e1c6e2ee0d7..d6d9c5c990f8 100644 --- a/core/res/res/anim/dream_activity_open_enter.xml +++ b/core/res/res/anim/dream_activity_open_enter.xml @@ -22,5 +22,5 @@ those two has to be the same. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="0.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml index 740f52856b7f..2c2e501eda69 100644 --- a/core/res/res/anim/dream_activity_open_exit.xml +++ b/core/res/res/anim/dream_activity_open_exit.xml @@ -22,4 +22,4 @@ dream_activity_open_enter animation. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index caa67de1fe61..173908d97b56 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2447,6 +2447,11 @@ <!-- Whether dreams are disabled when ambient mode is suppressed. --> <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool> + <!-- The duration in milliseconds of the dream opening animation. --> + <integer name="config_dreamOpenAnimationDuration">250</integer> + <!-- The duration in milliseconds of the dream closing animation. --> + <integer name="config_dreamCloseAnimationDuration">100</integer> + <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to assistant activities (ACTIVITY_TYPE_ASSISTANT) --> <bool name="config_dismissDreamOnActivityStart">false</bool> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d0fca8b0a7bb..509de3364f0e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -202,6 +202,11 @@ <!-- Displayed to tell the user that they cannot change the caller ID setting. --> <string name="CLIRPermanent">You can\'t change the caller ID setting.</string> + <!-- Notification title to tell the user that auto data switch has occurred. [CHAR LIMIT=NOTIF_TITLE] --> + <string name="auto_data_switch_title">Switched data to <xliff:g id="carrierDisplay" example="Verizon">%s</xliff:g></string> + <!-- Notification content to tell the user that auto data switch can be disabled at settings. [CHAR LIMIT=NOTIF_BODY] --> + <string name="auto_data_switch_content">You can change this anytime in Settings</string> + <!-- Notification title to tell the user that data service is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] --> <string name="RestrictedOnDataTitle">No mobile data service</string> <!-- Notification title to tell the user that emergency calling is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fc55ed2fe443..476d36d0c207 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -602,6 +602,8 @@ <java-symbol type="string" name="RestrictedOnEmergencyTitle" /> <java-symbol type="string" name="RestrictedOnNormalTitle" /> <java-symbol type="string" name="RestrictedStateContent" /> + <java-symbol type="string" name="auto_data_switch_title" /> + <java-symbol type="string" name="auto_data_switch_content" /> <java-symbol type="string" name="RestrictedStateContentMsimTemplate" /> <java-symbol type="string" name="notification_channel_network_alert" /> <java-symbol type="string" name="notification_channel_call_forward" /> @@ -2236,6 +2238,8 @@ <java-symbol type="string" name="config_dreamsDefaultComponent" /> <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" /> <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" /> + <java-symbol type="integer" name="config_dreamOpenAnimationDuration" /> + <java-symbol type="integer" name="config_dreamCloseAnimationDuration" /> <java-symbol type="array" name="config_supportedDreamComplications" /> <java-symbol type="array" name="config_disabledDreamComponents" /> <java-symbol type="bool" name="config_dismissDreamOnActivityStart" /> @@ -3723,7 +3727,6 @@ <java-symbol type="integer" name="config_maxUiWidth" /> <!-- system notification channels --> - <java-symbol type="string" name="notification_channel_virtual_keyboard" /> <java-symbol type="string" name="notification_channel_physical_keyboard" /> <java-symbol type="string" name="notification_channel_security" /> <java-symbol type="string" name="notification_channel_car_mode" /> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java new file mode 100644 index 000000000000..42143b92e9d8 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio.tests.unittests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; + +import android.hardware.radio.Announcement; +import android.hardware.radio.ProgramSelector; +import android.util.ArrayMap; + +import org.junit.Test; + +import java.util.Map; + +public final class RadioAnnouncementTest { + private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 90500); + private static final ProgramSelector FM_PROGRAM_SELECTOR = new ProgramSelector( + ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, /* secondaryIds= */ null, + /* vendorIds= */ null); + private static final int TRAFFIC_ANNOUNCEMENT_TYPE = Announcement.TYPE_TRAFFIC; + private static final Map<String, String> VENDOR_INFO = createVendorInfo(); + private static final Announcement TEST_ANNOUNCEMENT = + new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO); + + @Test + public void constructor_withNullSelector_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + new Announcement(/* selector= */ null, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO); + }); + + assertWithMessage("Exception for null program selector in announcement constructor") + .that(thrown).hasMessageThat().contains("Program selector cannot be null"); + } + + @Test + public void constructor_withNullVendorInfo_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, + /* vendorInfo= */ null); + }); + + assertWithMessage("Exception for null vendor info in announcement constructor") + .that(thrown).hasMessageThat().contains("Vendor info cannot be null"); + } + + @Test + public void getSelector() { + assertWithMessage("Radio announcement selector") + .that(TEST_ANNOUNCEMENT.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR); + } + + @Test + public void getType() { + assertWithMessage("Radio announcement type") + .that(TEST_ANNOUNCEMENT.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE); + } + + @Test + public void getVendorInfo() { + assertWithMessage("Radio announcement vendor info") + .that(TEST_ANNOUNCEMENT.getVendorInfo()).isEqualTo(VENDOR_INFO); + } + + private static Map<String, String> createVendorInfo() { + Map<String, String> vendorInfo = new ArrayMap<>(); + vendorInfo.put("vendorKeyMock", "vendorValueMock"); + return vendorInfo; + } +} diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java index 259a11852784..9bfa2fba6948 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java @@ -20,9 +20,12 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; +import android.hardware.radio.RadioMetadata; import org.junit.Test; +import java.util.Arrays; + public final class RadioManagerTest { private static final int REGION = RadioManager.REGION_ITU_2; @@ -63,6 +66,32 @@ public final class RadioManagerTest { private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig(); private static final RadioManager.ModuleProperties AMFM_PROPERTIES = createAmFmProperties(); + /** + * Info flags with live, tuned and stereo enabled + */ + private static final int INFO_FLAGS = 0b110001; + private static final int SIGNAL_QUALITY = 2; + private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, + /* value= */ 0x10000111); + private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, + /* value= */ 0x10000113); + private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, + /* value= */ 0x1013); + private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, + /* value= */ 95500); + private static final ProgramSelector DAB_SELECTOR = + new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER, + new ProgramSelector.Identifier[]{ + DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, + /* vendorIds= */ null); + private static final RadioMetadata METADATA = createMetadata(); + private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO = + createDabProgramInfo(DAB_SELECTOR); + @Test public void getType_forBandDescriptor() { RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); @@ -460,6 +489,123 @@ public final class RadioManagerTest { .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab); } + @Test + public void getSelector_forProgramInfo() { + assertWithMessage("Selector of DAB program info") + .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR); + } + + @Test + public void getLogicallyTunedTo_forProgramInfo() { + assertWithMessage("Identifier logically tuned to in DAB program info") + .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER); + } + + @Test + public void getPhysicallyTunedTo_forProgramInfo() { + assertWithMessage("Identifier physically tuned to DAB program info") + .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER); + } + + @Test + public void getRelatedContent_forProgramInfo() { + assertWithMessage("Related contents of DAB program info") + .that(DAB_PROGRAM_INFO.getRelatedContent()) + .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED); + } + + @Test + public void getChannel_forProgramInfo() { + assertWithMessage("Main channel of DAB program info") + .that(DAB_PROGRAM_INFO.getChannel()).isEqualTo(0); + } + + @Test + public void getSubChannel_forProgramInfo() { + assertWithMessage("Sub channel of DAB program info") + .that(DAB_PROGRAM_INFO.getSubChannel()).isEqualTo(0); + } + + @Test + public void isTuned_forProgramInfo() { + assertWithMessage("Tuned status of DAB program info") + .that(DAB_PROGRAM_INFO.isTuned()).isTrue(); + } + + @Test + public void isStereo_forProgramInfo() { + assertWithMessage("Stereo support in DAB program info") + .that(DAB_PROGRAM_INFO.isStereo()).isTrue(); + } + + @Test + public void isDigital_forProgramInfo() { + assertWithMessage("Digital DAB program info") + .that(DAB_PROGRAM_INFO.isDigital()).isTrue(); + } + + @Test + public void isLive_forProgramInfo() { + assertWithMessage("Live status of DAB program info") + .that(DAB_PROGRAM_INFO.isLive()).isTrue(); + } + + @Test + public void isMuted_forProgramInfo() { + assertWithMessage("Muted status of DAB program info") + .that(DAB_PROGRAM_INFO.isMuted()).isFalse(); + } + + @Test + public void isTrafficProgram_forProgramInfo() { + assertWithMessage("Traffic program support in DAB program info") + .that(DAB_PROGRAM_INFO.isTrafficProgram()).isFalse(); + } + + @Test + public void isTrafficAnnouncementActive_forProgramInfo() { + assertWithMessage("Active traffic announcement for DAB program info") + .that(DAB_PROGRAM_INFO.isTrafficAnnouncementActive()).isFalse(); + } + + @Test + public void getSignalStrength_forProgramInfo() { + assertWithMessage("Signal strength of DAB program info") + .that(DAB_PROGRAM_INFO.getSignalStrength()).isEqualTo(SIGNAL_QUALITY); + } + + @Test + public void getMetadata_forProgramInfo() { + assertWithMessage("Metadata of DAB program info") + .that(DAB_PROGRAM_INFO.getMetadata()).isEqualTo(METADATA); + } + + @Test + public void getVendorInfo_forProgramInfo() { + assertWithMessage("Vendor info of DAB program info") + .that(DAB_PROGRAM_INFO.getVendorInfo()).isEmpty(); + } + + @Test + public void equals_withSameProgramInfo_returnsTrue() { + RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR); + + assertWithMessage("The same program info") + .that(dabProgramInfoCompared).isEqualTo(DAB_PROGRAM_INFO); + } + + @Test + public void equals_withSameProgramInfoOfDifferentSecondaryIdSelectors_returnsFalse() { + ProgramSelector dabSelectorCompared = new ProgramSelector( + ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER, + new ProgramSelector.Identifier[]{DAB_FREQUENCY_IDENTIFIER}, + /* vendorIds= */ null); + RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(dabSelectorCompared); + + assertWithMessage("Program info with different secondary id selectors") + .that(DAB_PROGRAM_INFO).isNotEqualTo(dabProgramInfoCompared); + } + private static RadioManager.ModuleProperties createAmFmProperties() { return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID, IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, @@ -487,4 +633,16 @@ public final class RadioManagerTest { private static RadioManager.AmBandConfig createAmBandConfig() { return new RadioManager.AmBandConfig(createAmBandDescriptor()); } + + private static RadioMetadata createMetadata() { + RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder(); + return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest").build(); + } + + private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) { + return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER, + DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS, + SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null); + } + } diff --git a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java b/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java deleted file mode 100644 index 7c7cd12bcb73..000000000000 --- a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java +++ /dev/null @@ -1,56 +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 android.app.time; - -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class TimeConfigurationTest { - - @Test - public void testBuilder() { - TimeConfiguration first = new TimeConfiguration.Builder() - .setAutoDetectionEnabled(true) - .build(); - - assertThat(first.isAutoDetectionEnabled()).isTrue(); - - TimeConfiguration copyFromBuilderConfiguration = new TimeConfiguration.Builder(first) - .build(); - - assertThat(first).isEqualTo(copyFromBuilderConfiguration); - } - - @Test - public void testParcelable() { - TimeConfiguration.Builder builder = new TimeConfiguration.Builder(); - - assertRoundTripParcelable(builder.setAutoDetectionEnabled(true).build()); - - assertRoundTripParcelable(builder.setAutoDetectionEnabled(false).build()); - } - -} diff --git a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java index 3ab01f3d8832..e7d352cfee30 100644 --- a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java +++ b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java @@ -16,11 +16,9 @@ package android.app.time; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import android.os.ShellCommand; @@ -31,35 +29,12 @@ import org.junit.runner.RunWith; /** * Tests for non-SDK methods on {@link UnixEpochTime}. + * + * <p>See also {@link android.app.time.cts.UnixEpochTimeTest} for SDK methods. */ @RunWith(AndroidJUnit4.class) public class UnixEpochTimeTest { - @Test - public void testEqualsAndHashcode() { - UnixEpochTime one1000one = new UnixEpochTime(1000, 1); - assertEqualsAndHashCode(one1000one, one1000one); - - UnixEpochTime one1000two = new UnixEpochTime(1000, 1); - assertEqualsAndHashCode(one1000one, one1000two); - - UnixEpochTime two1000 = new UnixEpochTime(1000, 2); - assertNotEquals(one1000one, two1000); - - UnixEpochTime one2000 = new UnixEpochTime(2000, 1); - assertNotEquals(one1000one, one2000); - } - - private static void assertEqualsAndHashCode(Object one, Object two) { - assertEquals(one, two); - assertEquals(one.hashCode(), two.hashCode()); - } - - @Test - public void testParceling() { - assertRoundTripParcelable(new UnixEpochTime(1000, 1)); - } - @Test(expected = IllegalArgumentException.class) public void testParseCommandLineArg_noElapsedRealtime() { ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( @@ -91,22 +66,6 @@ public class UnixEpochTimeTest { } @Test - public void testAt() { - long timeMillis = 1000L; - int elapsedRealtimeMillis = 100; - UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMillis, timeMillis); - // Reference time is after the timestamp. - UnixEpochTime at125 = unixEpochTime.at(125); - assertEquals(timeMillis + (125 - elapsedRealtimeMillis), at125.getUnixEpochTimeMillis()); - assertEquals(125, at125.getElapsedRealtimeMillis()); - - // Reference time is before the timestamp. - UnixEpochTime at75 = unixEpochTime.at(75); - assertEquals(timeMillis + (75 - elapsedRealtimeMillis), at75.getUnixEpochTimeMillis()); - assertEquals(75, at75.getElapsedRealtimeMillis()); - } - - @Test public void testElapsedRealtimeDifference() { UnixEpochTime value1 = new UnixEpochTime(1000, 123L); assertEquals(0, UnixEpochTime.elapsedRealtimeDifference(value1, value1)); diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java index c8de190b30b0..ab63f1475f83 100644 --- a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java +++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java @@ -17,6 +17,9 @@ package android.service.timezone; import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -33,6 +36,32 @@ public class TimeZoneProviderEventTest { @Test public void isEquivalentToAndEquals() { + long creationElapsedMillis = 1111L; + TimeZoneProviderEvent failEvent = + TimeZoneProviderEvent.createPermanentFailureEvent(creationElapsedMillis, "one"); + TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN; + + TimeZoneProviderEvent uncertainEvent = + TimeZoneProviderEvent.createUncertainEvent(creationElapsedMillis, providerStatus); + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(creationElapsedMillis) + .setTimeZoneIds(Collections.singletonList("Europe/London")) + .build(); + TimeZoneProviderEvent suggestionEvent = TimeZoneProviderEvent.createSuggestionEvent( + creationElapsedMillis, suggestion, providerStatus); + + assertNotEquals(failEvent, uncertainEvent); + assertNotEquivalentTo(failEvent, uncertainEvent); + + assertNotEquals(failEvent, suggestionEvent); + assertNotEquivalentTo(failEvent, suggestionEvent); + + assertNotEquals(uncertainEvent, suggestionEvent); + assertNotEquivalentTo(uncertainEvent, suggestionEvent); + } + + @Test + public void isEquivalentToAndEquals_permanentFailure() { TimeZoneProviderEvent fail1v1 = TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "one"); assertEquals(fail1v1, fail1v1); @@ -51,44 +80,79 @@ public class TimeZoneProviderEventTest { assertNotEquals(fail1v1, fail2); assertIsEquivalentTo(fail1v1, fail2); } + } + + @Test + public void isEquivalentToAndEquals_uncertain() { + TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING) + .build(); + TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); - TimeZoneProviderEvent uncertain1v1 = TimeZoneProviderEvent.createUncertainEvent(1111L); + TimeZoneProviderEvent uncertain1v1 = + TimeZoneProviderEvent.createUncertainEvent(1111L, status1); assertEquals(uncertain1v1, uncertain1v1); assertIsEquivalentTo(uncertain1v1, uncertain1v1); assertNotEquals(uncertain1v1, null); assertNotEquivalentTo(uncertain1v1, null); { - TimeZoneProviderEvent uncertain1v2 = TimeZoneProviderEvent.createUncertainEvent(1111L); + TimeZoneProviderEvent uncertain1v2 = + TimeZoneProviderEvent.createUncertainEvent(1111L, status1); assertEquals(uncertain1v1, uncertain1v2); assertIsEquivalentTo(uncertain1v1, uncertain1v2); - TimeZoneProviderEvent uncertain2 = TimeZoneProviderEvent.createUncertainEvent(2222L); + TimeZoneProviderEvent uncertain2 = + TimeZoneProviderEvent.createUncertainEvent(2222L, status1); assertNotEquals(uncertain1v1, uncertain2); assertIsEquivalentTo(uncertain1v1, uncertain2); + + TimeZoneProviderEvent uncertain3 = + TimeZoneProviderEvent.createUncertainEvent(1111L, status2); + assertNotEquals(uncertain1v1, uncertain3); + assertNotEquivalentTo(uncertain1v1, uncertain3); } + } + @Test + public void isEquivalentToAndEquals_suggestion() { + TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING) + .build(); + TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); TimeZoneProviderSuggestion suggestion1 = new TimeZoneProviderSuggestion.Builder() .setElapsedRealtimeMillis(1111L) .setTimeZoneIds(Collections.singletonList("Europe/London")) .build(); TimeZoneProviderEvent certain1v1 = - TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1); + TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1); assertEquals(certain1v1, certain1v1); assertIsEquivalentTo(certain1v1, certain1v1); assertNotEquals(certain1v1, null); assertNotEquivalentTo(certain1v1, null); { - // Same suggestion, same time. + // Same time, suggestion, and status. TimeZoneProviderEvent certain1v2 = - TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1); + TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1); assertEquals(certain1v1, certain1v2); assertIsEquivalentTo(certain1v1, certain1v2); - // Same suggestion, different time. + // Different time, same suggestion and status. TimeZoneProviderEvent certain1v3 = - TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1); + TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status1); assertNotEquals(certain1v1, certain1v3); assertIsEquivalentTo(certain1v1, certain1v3); @@ -100,7 +164,7 @@ public class TimeZoneProviderEventTest { assertNotEquals(suggestion1, suggestion2); TimeZoneProviderSuggestionTest.assertIsEquivalentTo(suggestion1, suggestion2); TimeZoneProviderEvent certain2 = - TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2); + TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2, status1); assertNotEquals(certain1v1, certain2); assertIsEquivalentTo(certain1v1, certain2); @@ -109,16 +173,15 @@ public class TimeZoneProviderEventTest { .setTimeZoneIds(Collections.singletonList("Europe/Paris")) .build(); TimeZoneProviderEvent certain3 = - TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3); + TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3, status1); assertNotEquals(certain1v1, certain3); assertNotEquivalentTo(certain1v1, certain3); - } - assertNotEquals(fail1v1, uncertain1v1); - assertNotEquivalentTo(fail1v1, uncertain1v1); - - assertNotEquals(fail1v1, certain1v1); - assertNotEquivalentTo(fail1v1, certain1v1); + TimeZoneProviderEvent certain4 = + TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status2); + assertNotEquals(certain1v1, certain4); + assertNotEquivalentTo(certain1v1, certain4); + } } @Test @@ -130,7 +193,8 @@ public class TimeZoneProviderEventTest { @Test public void testParcelable_uncertain() { - TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(1111L); + TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent( + 1111L, TimeZoneProviderStatus.UNKNOWN); assertRoundTripParcelable(event); } @@ -139,8 +203,8 @@ public class TimeZoneProviderEventTest { TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() .setTimeZoneIds(Arrays.asList("Europe/London", "Europe/Paris")) .build(); - TimeZoneProviderEvent event = - TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion); + TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent( + 1111L, suggestion, TimeZoneProviderStatus.UNKNOWN); assertRoundTripParcelable(event); } diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java new file mode 100644 index 000000000000..d61c33c935db --- /dev/null +++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.timezone; + +import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class TimeZoneProviderStatusTest { + + @Test + public void testStatusValidation() { + TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(DEPENDENCY_STATUS_WORKING) + .build(); + + assertThrows(IllegalArgumentException.class, + () -> new TimeZoneProviderStatus.Builder(status) + .setLocationDetectionStatus(-1) + .build()); + assertThrows(IllegalArgumentException.class, + () -> new TimeZoneProviderStatus.Builder(status) + .setConnectivityStatus(-1) + .build()); + assertThrows(IllegalArgumentException.class, + () -> new TimeZoneProviderStatus.Builder(status) + .setTimeZoneResolutionStatus(-1) + .build()); + } + + @Test + public void testEqualsAndHashcode() { + TimeZoneProviderStatus status1_1 = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING) + .build(); + assertEqualsAndHashcode(status1_1, status1_1); + assertNotEquals(status1_1, null); + + { + TimeZoneProviderStatus status1_2 = + new TimeZoneProviderStatus.Builder(status1_1).build(); + assertEqualsAndHashcode(status1_1, status1_2); + assertNotSame(status1_1, status1_2); + } + + { + TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) + .setLocationDetectionStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) + .build(); + assertNotEquals(status1_1, status2); + } + + { + TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) + .setConnectivityStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) + .build(); + assertNotEquals(status1_1, status2); + } + + { + TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); + assertNotEquals(status1_1, status2); + } + } + + private static void assertEqualsAndHashcode(Object one, Object two) { + assertEquals(one, two); + assertEquals(two, one); + assertEquals(one.hashCode(), two.hashCode()); + } + + @Test + public void testParcelable() { + TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); + assertRoundTripParcelable(status); + } +} diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index f448cb3091e7..f370ebd94545 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -60,6 +60,8 @@ public class WindowOnBackInvokedDispatcherTest { private OnBackAnimationCallback mCallback1; @Mock private OnBackAnimationCallback mCallback2; + @Mock + private BackEvent mBackEvent; @Before public void setUp() throws Exception { @@ -85,14 +87,14 @@ public class WindowOnBackInvokedDispatcherTest { verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo( Mockito.eq(mWindow), captor.capture()); - captor.getAllValues().get(0).getCallback().onBackStarted(); + captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); verifyZeroInteractions(mCallback2); - captor.getAllValues().get(1).getCallback().onBackStarted(); + captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); verifyNoMoreInteractions(mCallback1); } @@ -110,9 +112,9 @@ public class WindowOnBackInvokedDispatcherTest { Mockito.eq(mWindow), captor.capture()); verifyNoMoreInteractions(mWindowSession); assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); } @Test @@ -148,8 +150,8 @@ public class WindowOnBackInvokedDispatcherTest { mDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture()); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 699e7947410e..decfb9fc59df 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -265,6 +265,7 @@ applications that come with the platform <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/> <permission name="android.permission.INSTALL_PACKAGES"/> <permission name="android.permission.INSTALL_PACKAGE_UPDATES"/> + <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/> <!-- Needed for test only --> <permission name="android.permission.ACCESS_MTP"/> <!-- Needed for test only --> diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index 1c41d06a3da2..9940ca3933c8 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -47,7 +47,7 @@ public final class BLASTBufferQueue { TransactionHangCallback callback); public interface TransactionHangCallback { - void onTransactionHang(boolean isGpuHang); + void onTransactionHang(String reason); } /** Create a new connection with the surface flinger. */ 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 43f39b78ca1a..db5de431cced 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 @@ -76,10 +76,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont "persist.wm.debug.predictive_back_progress_threshold"; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", - SETTING_VALUE_ON) != SETTING_VALUE_OFF; + SETTING_VALUE_ON) != SETTING_VALUE_ON; private static final int PROGRESS_THRESHOLD = SystemProperties .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); - + /** Flag for U animation features */ + public static boolean IS_U_ANIMATION_ENABLED = + SystemProperties.getInt("persist.wm.debug.predictive_back_anim", + SETTING_VALUE_OFF) == SETTING_VALUE_ON; + /** Predictive back animation developer option */ + private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); // TODO (b/241808055) Find a appropriate time to remove during refactor private static final boolean ENABLE_SHELL_TRANSITIONS = Transitions.ENABLE_SHELL_TRANSITIONS; /** @@ -88,8 +93,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ private static final long MAX_TRANSITION_DURATION = 2000; - private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); - /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; @@ -143,53 +146,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } }; - /** - * Helper class to record the touch location for gesture start and latest. - */ - private static class TouchTracker { - /** - * Location of the latest touch event - */ - private float mLatestTouchX; - private float mLatestTouchY; - private int mSwipeEdge; - private float mProgressThreshold; - - /** - * Location of the initial touch event of the back gesture. - */ - private float mInitTouchX; - private float mInitTouchY; - - void update(float touchX, float touchY, int swipeEdge) { - mLatestTouchX = touchX; - mLatestTouchY = touchY; - mSwipeEdge = swipeEdge; - } - - void setGestureStartLocation(float touchX, float touchY) { - mInitTouchX = touchX; - mInitTouchY = touchY; - } - - void setProgressThreshold(float progressThreshold) { - mProgressThreshold = progressThreshold; - } - - float getProgress(float touchX) { - int deltaX = Math.round(touchX - mInitTouchX); - float progressThreshold = PROGRESS_THRESHOLD >= 0 - ? PROGRESS_THRESHOLD : mProgressThreshold; - return Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); - } - - void reset() { - mInitTouchX = 0; - mInitTouchY = 0; - mSwipeEdge = -1; - } - } - public BackAnimationController( @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -221,6 +177,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransitions = transitions; } + @VisibleForTesting + void setEnableUAnimation(boolean enable) { + IS_U_ANIMATION_ENABLED = enable; + } + private void onInit() { setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); createAdapter(); @@ -374,7 +335,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mTransitionInProgress) { return; } - mTouchTracker.update(touchX, touchY, swipeEdge); + + mTouchTracker.update(touchX, touchY); if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; @@ -384,7 +346,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. - onGestureStarted(touchX, touchY); + onGestureStarted(touchX, touchY, swipeEdge); mShouldStartOnNextMoveEvent = false; } onMove(touchX, touchY, swipeEdge); @@ -398,14 +360,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private void onGestureStarted(float touchX, float touchY) { + private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); finishBackNavigation(); } - mTouchTracker.setGestureStartLocation(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); mBackGestureStarted = true; try { @@ -428,12 +390,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final IOnBackInvokedCallback targetCallback; final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType); if (shouldDispatchToAnimator) { - targetCallback = mAnimationDefinition.get(backType).getGestureStartedCallback(); + mAnimationDefinition.get(backType).startGesture(); } else { targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); - } - if (shouldDispatchToAnimator) { - dispatchOnBackStarted(targetCallback); + dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null)); } } @@ -441,12 +401,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) { return; } - mTouchTracker.update(touchX, touchY, swipeEdge); - float progress = mTouchTracker.getProgress(touchX); - int backType = mBackNavigationInfo.getType(); + final BackEvent backEvent = mTouchTracker.createProgressEvent(); - BackEvent backEvent = new BackEvent(touchX, touchY, progress, swipeEdge); - IOnBackInvokedCallback targetCallback = null; + int backType = mBackNavigationInfo.getType(); + IOnBackInvokedCallback targetCallback; if (shouldDispatchToAnimator(backType)) { targetCallback = mAnimationDefinition.get(backType).getCallback(); } else { @@ -532,18 +490,21 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && mAnimationDefinition.contains(backType); } - private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { + private void dispatchOnBackStarted(IOnBackInvokedCallback callback, + BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackStarted(); + if (shouldDispatchAnimation(callback)) { + callback.onBackStarted(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } } - private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { + private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; } @@ -554,29 +515,39 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { if (callback == null) { return; } try { - callback.onBackCancelled(); + if (shouldDispatchAnimation(callback)) { + callback.onBackCancelled(); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } } - private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback, + private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackProgressed(backEvent); + if (shouldDispatchAnimation(callback)) { + callback.onBackProgressed(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } + private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) { + return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get( + BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback()) + && mEnableAnimations.get(); + } + /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ @@ -585,6 +556,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } mTriggerBack = triggerBack; + mTouchTracker.setTriggerBack(triggerBack); } private void setSwipeThresholds(float triggerThreshold, float progressThreshold) { @@ -670,13 +642,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()"); runner.startAnimation(apps, wallpapers, nonApps, BackAnimationController.this::onBackAnimationFinished); + if (apps.length >= 1) { + final int backType = mBackNavigationInfo.getType(); + IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType) + .getCallback(); + dispatchOnBackStarted( + targetCallback, mTouchTracker.createStartEvent(apps[0])); + } if (!mBackGestureStarted) { // if the down -> up gesture happened before animation start, we have to // trigger the uninterruptible transition to finish the back animation. - final BackEvent backFinish = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1, - mTouchTracker.mSwipeEdge); + final BackEvent backFinish = mTouchTracker.createProgressEvent(1); startTransition(); runner.consumeIfGestureFinished(backFinish); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index 12bbf73af561..c53fcfc99c9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -79,9 +79,8 @@ class BackAnimationRunner { } } - IOnBackInvokedCallback getGestureStartedCallback() { + void startGesture() { mWaitingAnimation = true; - return mCallback; } boolean onGestureFinished(boolean triggerBack) { 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 new file mode 100644 index 000000000000..ccfac65d6342 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import android.os.SystemProperties; +import android.view.RemoteAnimationTarget; +import android.window.BackEvent; + +/** + * Helper class to record the touch location for gesture and generate back events. + */ +class TouchTracker { + private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = + "persist.wm.debug.predictive_back_progress_threshold"; + private static final int PROGRESS_THRESHOLD = SystemProperties + .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + private float mProgressThreshold; + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private boolean mTriggerBack; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + private float mStartThresholdX; + private int mSwipeEdge; + private boolean mCancelled; + + void update(float touchX, float touchY) { + /** + * If back was previously cancelled but the user has started swiping in the forward + * direction again, restart back. + */ + if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT) + || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { + mCancelled = false; + mStartThresholdX = touchX; + } + mLatestTouchX = touchX; + mLatestTouchY = touchY; + } + + void setTriggerBack(boolean triggerBack) { + if (mTriggerBack != triggerBack && !triggerBack) { + mCancelled = true; + } + mTriggerBack = triggerBack; + } + + void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { + mInitTouchX = touchX; + mInitTouchY = touchY; + mSwipeEdge = swipeEdge; + mStartThresholdX = mInitTouchX; + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + mStartThresholdX = 0; + mCancelled = false; + mTriggerBack = false; + mSwipeEdge = BackEvent.EDGE_LEFT; + } + + BackEvent createStartEvent(RemoteAnimationTarget target) { + return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); + } + + BackEvent createProgressEvent() { + float progressThreshold = PROGRESS_THRESHOLD >= 0 + ? PROGRESS_THRESHOLD : mProgressThreshold; + progressThreshold = progressThreshold == 0 ? 1 : progressThreshold; + float progress = 0; + // Progress is always 0 when back is cancelled and not restarted. + if (!mCancelled) { + // If back is committed, progress is the distance between the last and first touch + // point, divided by the max drag distance. Otherwise, it's the distance between + // the last touch point and the starting threshold, divided by max drag distance. + // The starting threshold is initially the first touch location, and updated to + // the location everytime back is restarted after being cancelled. + float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; + float deltaX = Math.max( + mSwipeEdge == BackEvent.EDGE_LEFT + ? mLatestTouchX - startX + : startX - mLatestTouchX, + 0); + progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1); + } + return createProgressEvent(progress); + } + + BackEvent createProgressEvent(float progress) { + return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); + } + + public void setProgressThreshold(float progressThreshold) { + mProgressThreshold = progressThreshold; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java index d6803e8052c6..d3a9a672ec76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java @@ -52,7 +52,7 @@ public class BubbleBadgeIconFactory extends BaseIconFactory { userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon); } Bitmap userBadgedBitmap = createIconBitmap( - userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW); + userBadgedAppIcon, 1, MODE_WITH_SHADOW); return createIconBitmap(userBadgedBitmap); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java index 5dab8a071f76..4ded3ea951e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java @@ -79,6 +79,6 @@ public class BubbleIconFactory extends BaseIconFactory { true /* shrinkNonAdaptiveIcons */, null /* outscale */, outScale); - return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW); + return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index afb64c9eec41..43d3f36f1fe5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -60,7 +60,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, FloatingContentCoordinator.FloatingContent { public static final boolean ENABLE_FLING_TO_DISMISS_PIP = - SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true); + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false); private static final String TAG = "PipMotionHelper"; private static final boolean DEBUG = false; 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 8cee4f1dc8fb..6ce981e25f5e 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 @@ -432,7 +432,8 @@ public class SplashscreenContentDrawer { final ShapeIconFactory factory = new ShapeIconFactory( SplashscreenContentDrawer.this.mContext, scaledIconDpi, mFinalIconSize); - final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); + final Bitmap bitmap = factory.createScaledBitmap(iconDrawable, + BaseIconFactory.MODE_DEFAULT); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); createIconDrawable(new BitmapDrawable(bitmap), true, mHighResIconProvider.mLoadInDetail); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 2b27baeb515a..66d0a2aa409b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -16,8 +16,6 @@ package com.android.wm.shell.transition; -import static android.hardware.HardwareBuffer.RGBA_8888; -import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; import static android.util.RotationUtils.deltaRotation; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; @@ -37,8 +35,6 @@ import android.graphics.ColorSpace; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; -import android.media.Image; -import android.media.ImageReader; import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; @@ -50,12 +46,11 @@ import android.window.ScreenCapture; import android.window.TransitionInfo; import com.android.internal.R; +import com.android.internal.policy.TransitionAnimation; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; /** * This class handles the rotation animation when the device is rotated. @@ -173,7 +168,7 @@ class ScreenRotationAnimation { t.setBuffer(mScreenshotLayer, hardwareBuffer); t.show(mScreenshotLayer); if (!isCustomRotate()) { - mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace); + mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace); } } @@ -404,93 +399,6 @@ class ScreenRotationAnimation { mTransactionPool.release(t); } - /** - * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the - * luminance at the borders of the bitmap - * @return the average luminance of all the pixels at the borders of the bitmap - */ - private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) { - // Cannot read content from buffer with protected usage. - if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888 - || hasProtectedContent(hardwareBuffer)) { - return 0; - } - - ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(), - hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1); - ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace); - Image image = ir.acquireLatestImage(); - if (image == null || image.getPlanes().length == 0) { - return 0; - } - - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int width = image.getWidth(); - int height = image.getHeight(); - int pixelStride = plane.getPixelStride(); - int rowStride = plane.getRowStride(); - float[] borderLumas = new float[2 * width + 2 * height]; - - // Grab the top and bottom borders - int l = 0; - for (int x = 0; x < width; x++) { - borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); - } - - // Grab the left and right borders - for (int y = 0; y < height; y++) { - borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); - } - - // Cleanup - ir.close(); - - // Oh, is this too simple and inefficient for you? - // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians - Arrays.sort(borderLumas); - return borderLumas[borderLumas.length / 2]; - } - - /** - * @return whether the hardwareBuffer passed in is marked as protected. - */ - private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { - return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; - } - - private static float getPixelLuminance(ByteBuffer buffer, int x, int y, - int pixelStride, int rowStride) { - int offset = y * rowStride + x * pixelStride; - int pixel = 0; - pixel |= (buffer.get(offset) & 0xff) << 16; // R - pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G - pixel |= (buffer.get(offset + 2) & 0xff); // B - pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A - return Color.valueOf(pixel).luminance(); - } - - /** - * Gets the average border luma by taking a screenshot of the {@param surfaceControl}. - * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace) - */ - private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) { - if (surfaceControl == null) { - return 0; - } - - Rect crop = new Rect(0, 0, bounds.width(), bounds.height()); - ScreenCapture.ScreenshotHardwareBuffer buffer = - ScreenCapture.captureLayers(surfaceControl, crop, 1); - if (buffer == null) { - return 0; - } - - return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); - } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index e90389764af3..f209521b1da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -33,6 +33,8 @@ public class SplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -62,8 +64,12 @@ public class SplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } public SplitBounds(Parcel parcel) { @@ -75,6 +81,8 @@ public class SplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class SplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 7d1f130daaef..9d61c14e1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -101,9 +101,9 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final int shadowRadiusID = taskInfo.isFocused ? R.dimen.freeform_decor_shadow_focused_thickness : R.dimen.freeform_decor_shadow_unfocused_thickness; - final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -114,6 +114,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL int outsetRightId = R.dimen.freeform_resize_handle; int outsetBottomId = R.dimen.freeform_resize_handle; + mRelayoutParams.reset(); mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 01cab9aae312..b314163802ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -91,7 +91,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mTaskBackgroundSurface; SurfaceControl mCaptionContainerSurface; - private CaptionWindowManager mCaptionWindowManager; + private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private final Rect mCaptionInsetsRect = new Rect(); @@ -199,13 +199,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - final int decorContainerOffsetX = -loadResource(params.mOutsetLeftId); - final int decorContainerOffsetY = -loadResource(params.mOutsetTopId); + final Resources resources = mDecorWindowContext.getResources(); + final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() - + loadResource(params.mOutsetRightId) + + loadDimensionPixelSize(resources, params.mOutsetRightId) - decorContainerOffsetX; outResult.mHeight = taskBounds.height() - + loadResource(params.mOutsetBottomId) + + loadDimensionPixelSize(resources, params.mOutsetBottomId) - decorContainerOffsetY; startT.setPosition( mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) @@ -225,7 +226,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - float shadowRadius = loadResource(params.mShadowRadiusId); + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; @@ -248,8 +249,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = loadResource(params.mCaptionHeightId); - final int captionWidth = loadResource(params.mCaptionWidthId); + final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); //Prevent caption from going offscreen if task is too high up final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; @@ -264,8 +265,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new CaptionWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface); + mCaptionWindowManager = new WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); } // Caption view @@ -309,13 +311,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setCrop(mTaskSurface, mTaskSurfaceCrop); } - private int loadResource(int resourceId) { - if (resourceId == Resources.ID_NULL) { - return 0; - } - return mDecorWindowContext.getResources().getDimensionPixelSize(resourceId); - } - /** * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or * registers {@link #mOnDisplaysChangedListener} if it doesn't. @@ -374,33 +369,18 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } - static class RelayoutResult<T extends View & TaskFocusStateConsumer> { - int mWidth; - int mHeight; - T mRootView; - - void reset() { - mWidth = 0; - mHeight = 0; - mRootView = null; - } - } - - private static class CaptionWindowManager extends WindowlessWindowManager { - CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); + private static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; } + return resources.getDimensionPixelSize(resourceId); } - interface SurfaceControlViewHostFactory { - default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { - return new SurfaceControlViewHost(c, d, wmm); + private static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; } + return resources.getDimension(resourceId); } static class RelayoutParams{ @@ -433,6 +413,23 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetLeftId = Resources.ID_NULL; mOutsetRightId = Resources.ID_NULL; } + } + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { + int mWidth; + int mHeight; + T mRootView; + + void reset() { + mWidth = 0; + mHeight = 0; + mRootView = null; + } + } + + interface SurfaceControlViewHostFactory { + default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { + return new SurfaceControlViewHost(c, d, wmm); + } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index fa783f231607..45eae2e2fe40 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -26,7 +26,6 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd @@ -35,7 +34,6 @@ import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible import com.android.wm.shell.flicker.splitScreenDismissed import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible -import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -95,7 +93,9 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible( primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) - private fun secondaryAppBoundsIsFullscreenAtEnd_internal() { + @Presubmit + @Test + fun secondaryAppBoundsIsFullscreenAtEnd() { testSpec.assertLayers { this.isVisible(secondaryApp) .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) @@ -117,20 +117,6 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen @Presubmit @Test - fun secondaryAppBoundsIsFullscreenAtEnd() { - Assume.assumeFalse(isShellTransitionsEnabled) - secondaryAppBoundsIsFullscreenAtEnd_internal() - } - - @FlakyTest(bugId = 250528485) - @Test - fun secondaryAppBoundsIsFullscreenAtEnd_shellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) - secondaryAppBoundsIsFullscreenAtEnd_internal() - } - - @Presubmit - @Test fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp) @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index 84a8c0a59f32..73159c981b82 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.splitscreen -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit @@ -146,19 +145,15 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB // robust enough to get the correct end state. } - @FlakyTest(bugId = 241524174) @Test fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) - @FlakyTest(bugId = 241524174) @Test fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) - @FlakyTest(bugId = 241524174) @Test fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp) - @FlakyTest(bugId = 241524174) @Test fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( primaryApp, @@ -166,9 +161,6 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = true ) - // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to - // get the correct end state. - @FlakyTest(bugId = 246490534) @Test fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( secondaryApp, @@ -176,11 +168,9 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = false ) - @FlakyTest(bugId = 241524174) @Test fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) - @FlakyTest(bugId = 241524174) @Test fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp) diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..8949a75d1a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> +</resources>
\ No newline at end of file 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 6484b0759bd7..7896247c5f5a 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 @@ -128,6 +128,7 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellExecutor, new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, mContentResolver, mTransitions); + mController.setEnableUAnimation(true); mShellInit.init(); mEventTime = 0; mShellExecutor.flushAll(); @@ -206,10 +207,9 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); - ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back @@ -236,11 +236,11 @@ public class BackAnimationControllerTest extends ShellTestCase { triggerBackGesture(); - verify(appCallback, never()).onBackStarted(); + verify(appCallback, never()).onBackStarted(any(BackEvent.class)); verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(appCallback, times(1)).onBackInvoked(); - verify(mIOnBackInvokedCallback, never()).onBackStarted(); + verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class)); verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mIOnBackInvokedCallback, never()).onBackInvoked(); verify(mBackAnimationRunner, never()).onAnimationStart( @@ -279,7 +279,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); } @@ -301,9 +301,8 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); } @@ -318,7 +317,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); // Check that back invocation is dispatched. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java new file mode 100644 index 000000000000..3aefc3f03a8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import static org.junit.Assert.assertEquals; + +import android.window.BackEvent; + +import org.junit.Before; +import org.junit.Test; + +public class TouchTrackerTest { + private static final float FAKE_THRESHOLD = 400; + private static final float INITIAL_X_LEFT_EDGE = 5; + private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE; + private TouchTracker mTouchTracker; + + @Before + public void setUp() throws Exception { + mTouchTracker = new TouchTracker(); + mTouchTracker.setProgressThreshold(FAKE_THRESHOLD); + } + + @Test + public void generatesProgress_onStart() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + BackEvent event = mTouchTracker.createStartEvent(null); + assertEquals(event.getProgress(), 0f, 0f); + } + + @Test + public void generatesProgress_leftEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + float touchX = 10; + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX += 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX -= 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX += 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + } + + @Test + public void generatesProgress_rightEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT); + float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX -= 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX += 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX -= 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + } + + private float getProgress() { + return mTouchTracker.createProgressEvent().getProgress(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 103c8dab17d5..4d37e5dbc4dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -50,12 +50,13 @@ import android.view.WindowManager.LayoutParams; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; -import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.tests.R; import org.junit.Before; import org.junit.Test; @@ -145,8 +146,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -196,13 +200,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; -// int outsetLeftId = R.dimen.split_divider_bar_width; -// int outsetTopId = R.dimen.gestures_onehanded_drag_threshold; -// int outsetRightId = R.dimen.freeform_resize_handle; -// int outsetBottomId = R.dimen.bubble_dismiss_target_padding_x; -// mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -211,8 +213,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(decorContainerSurfaceBuilder).setParent(taskSurface); verify(decorContainerSurfaceBuilder).setContainerLayer(); verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); - verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -60, -60); - verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 420, 220); + verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40); + verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220); verify(taskBackgroundSurfaceBuilder).setParent(taskSurface); verify(taskBackgroundSurfaceBuilder).setEffectLayer(); @@ -225,36 +227,34 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -6, -156); - verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 432); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8); + verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); verify(mMockSurfaceControlViewHost) .setView(same(mMockView), - argThat(lp -> lp.height == 432 + argThat(lp -> lp.height == 64 && lp.width == 432 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); if (ViewRootImpl.CAPTION_ON_SHELL) { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 516), + new Rect(100, 300, 400, 332), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } verify(mMockSurfaceControlFinishT) .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y); verify(mMockSurfaceControlFinishT) - .setCrop(taskSurface, new Rect(-60, -60, 360, 160)); + .setCrop(taskSurface, new Rect(-20, -40, 360, 180)); verify(mMockSurfaceControlStartT) .show(taskSurface); - assertEquals(420, mRelayoutResult.mWidth); + assertEquals(380, mRelayoutResult.mWidth); assertEquals(220, mRelayoutResult.mHeight); - - } @Test @@ -293,8 +293,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle, - R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -365,7 +368,8 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, testSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), @@ -417,12 +421,10 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - mRelayoutParams.mLayoutResId = 0; - mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_width; - mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; - mRelayoutParams.mShadowRadiusId = - R.dimen.freeform_decor_shadow_unfocused_thickness; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 2547a963eb31..d975e96f193b 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -6632,8 +6632,8 @@ public class AudioManager { } } if (k == ports.size()) { - // this hould never happen - Log.e(TAG, "updatePortConfig port not found for handle: "+port.handle().id()); + // This can happen in case of stale audio patch referring to a removed device and is + // handled by the caller. return null; } AudioGainConfig gainCfg = portCfg.gain(); diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java index 39b3d0b47a27..0291f64c0640 100644 --- a/media/java/android/media/ImageWriter.java +++ b/media/java/android/media/ImageWriter.java @@ -264,10 +264,9 @@ public class ImageWriter implements AutoCloseable { if (useSurfaceImageFormatInfo) { // nativeInit internally overrides UNKNOWN format. So does surface format query after // nativeInit and before getEstimatedNativeAllocBytes(). - imageFormat = SurfaceUtils.getSurfaceFormat(surface); - mDataSpace = dataSpace = PublicFormatUtils.getHalDataspace(imageFormat); - mHardwareBufferFormat = - hardwareBufferFormat = PublicFormatUtils.getHalFormat(imageFormat); + mHardwareBufferFormat = hardwareBufferFormat = SurfaceUtils.getSurfaceFormat(surface); + mDataSpace = dataSpace = SurfaceUtils.getSurfaceDataspace(surface); + imageFormat = PublicFormatUtils.getPublicFormat(hardwareBufferFormat, dataSpace); } // Estimate the native buffer allocation size and register it so it gets accounted for diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index 74c549943a74..ee825881ecae 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -240,8 +240,7 @@ public final class MidiManager { * @param handler The {@link android.os.Handler Handler} that will be used for delivering the * device notifications. If handler is null, then the thread used for the * callback is unspecified. - * @deprecated Use the {@link #registerDeviceCallback} - * method with Executor and transport instead. + * @deprecated Use {@link #registerDeviceCallback(int, Executor, DeviceCallback)} instead. */ @Deprecated public void registerDeviceCallback(DeviceCallback callback, Handler handler) { diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp index 1e56a9340294..6990ad0fbd7d 100644 --- a/packages/CarrierDefaultApp/Android.bp +++ b/packages/CarrierDefaultApp/Android.bp @@ -10,7 +10,7 @@ package { android_app { name: "CarrierDefaultApp", srcs: ["src/**/*.java"], - static_libs: ["SliceStore"], + libs: ["SliceStore"], platform_apis: true, certificate: "platform", } diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 9566f22a7274..a5b104b597ee 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -28,6 +28,7 @@ <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" /> <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <application android:label="@string/app_name" diff --git a/packages/CompanionDeviceManager/TEST_MAPPING b/packages/CompanionDeviceManager/TEST_MAPPING deleted file mode 100644 index 63f54fa35158..000000000000 --- a/packages/CompanionDeviceManager/TEST_MAPPING +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presubmit": [ - { - "name": "CtsOsTestCases", - "options": [ - { - "include-filter": "android.os.cts.CompanionDeviceManagerTest" - } - ] - } - ] -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index ec0c5b708abe..64a12cd12f35 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -16,22 +16,24 @@ package com.android.credentialmanager -import android.app.Activity import android.app.slice.Slice import android.app.slice.SliceSpec import android.content.Context import android.content.Intent +import android.credentials.CreateCredentialRequest import android.credentials.ui.Constants import android.credentials.ui.Entry import android.credentials.ui.ProviderData import android.credentials.ui.RequestInfo -import android.credentials.ui.UserSelectionResult +import android.credentials.ui.BaseDialogResult +import android.credentials.ui.UserSelectionDialogResult import android.graphics.drawable.Icon import android.os.Binder import android.os.Bundle import android.os.ResultReceiver import com.android.credentialmanager.createflow.CreatePasskeyUiState import com.android.credentialmanager.createflow.CreateScreenState +import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState @@ -49,11 +51,7 @@ class CredentialManagerRepo( requestInfo = intent.extras?.getParcelable( RequestInfo.EXTRA_REQUEST_INFO, RequestInfo::class.java - ) ?: RequestInfo( - Binder(), - RequestInfo.TYPE_CREATE, - /*isFirstUsage=*/false - ) + ) ?: testRequestInfo() providerList = intent.extras?.getParcelableArrayList( ProviderData.EXTRA_PROVIDER_DATA_LIST, @@ -67,21 +65,20 @@ class CredentialManagerRepo( } fun onCancel() { - resultReceiver?.send(Activity.RESULT_CANCELED, null) + val resultData = Bundle() + BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData) + resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_CANCELED, resultData) } fun onOptionSelected(providerPackageName: String, entryId: Int) { - val userSelectionResult = UserSelectionResult( + val userSelectionDialogResult = UserSelectionDialogResult( requestInfo.token, providerPackageName, entryId ) val resultData = Bundle() - resultData.putParcelable( - UserSelectionResult.EXTRA_USER_SELECTION_RESULT, - userSelectionResult - ) - resultReceiver?.send(Activity.RESULT_OK, resultData) + UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData) + resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, resultData) } fun getCredentialInitialUiState(): GetCredentialUiState { @@ -95,9 +92,12 @@ class CredentialManagerRepo( fun createPasskeyInitialUiState(): CreatePasskeyUiState { val providerList = CreateFlowUtils.toProviderList(providerList, context) + val requestDisplayInfo = RequestDisplayInfo( + "Elisa Beckett", "beckett-bakert@gmail.com", "TYPE_CREATE") return CreatePasskeyUiState( providers = providerList, currentScreenState = CreateScreenState.PASSKEY_INTRO, + requestDisplayInfo, ) } @@ -175,4 +175,18 @@ class CredentialManagerRepo( slice ) } + + private fun testRequestInfo(): RequestInfo { + val data = Bundle() + return RequestInfo.newCreateRequestInfo( + Binder(), + CreateCredentialRequest( + // TODO: use the jetpack type and utils once defined. + "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL", + data + ), + /*isFirstUsage=*/false, + "tribank.us" + ) + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 19820d6cd98c..e291cc206691 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -34,6 +34,21 @@ data class CreateOptionInfo( val usageData: String ) +data class RequestDisplayInfo( + val userName: String, + val displayName: String, + val type: String, +) + +/** + * This is initialized to be the most recent used. Can then be changed if + * user selects a different entry on the more option page. + */ +data class ActiveEntry ( + val activeProvider: ProviderInfo, + val activeCreateOptionInfo: CreateOptionInfo, +) + /** The name of the current screen. */ enum class CreateScreenState { PASSKEY_INTRO, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index 82fce9f7a98d..fbbc3ac20e33 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -70,21 +70,21 @@ fun CreatePasskeyScreen( onProviderSelected = {viewModel.onProviderSelected(it)} ) CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( - providerInfo = uiState.selectedProvider!!, - onOptionSelected = {viewModel.onCreateOptionSelected(it)}, + requestDisplayInfo = uiState.requestDisplayInfo, + providerInfo = uiState.activeEntry?.activeProvider!!, + onOptionSelected = {viewModel.onPrimaryCreateOptionInfoSelected()}, onCancel = {viewModel.onCancel()}, multiProvider = uiState.providers.size > 1, - onMoreOptionsSelected = {viewModel.onMoreOptionsSelected(it)} + onMoreOptionsSelected = {viewModel.onMoreOptionsSelected()} ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( - providerInfo = uiState.selectedProvider!!, providerList = uiState.providers, - onBackButtonSelected = {viewModel.onBackButtonSelected(it)}, + onBackButtonSelected = {viewModel.onBackButtonSelected()}, onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)} ) CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( - providerInfo = uiState.selectedProvider!!, - onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected(it)} + providerInfo = uiState.activeEntry?.activeProvider!!, + onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected()} ) } }, @@ -218,10 +218,9 @@ fun ProviderSelectionCard( @ExperimentalMaterialApi @Composable fun MoreOptionsSelectionCard( - providerInfo: ProviderInfo, providerList: List<ProviderInfo>, - onBackButtonSelected: (String) -> Unit, - onOptionSelected: (String) -> Unit + onBackButtonSelected: () -> Unit, + onOptionSelected: (ActiveEntry) -> Unit ) { Card( backgroundColor = lightBackgroundColor, @@ -235,7 +234,7 @@ fun MoreOptionsSelectionCard( elevation = 0.dp, navigationIcon = { - IconButton(onClick = { onBackButtonSelected(providerInfo.name) }) { + IconButton(onClick = onBackButtonSelected) { Icon(Icons.Filled.ArrowBack, "backIcon" ) } @@ -264,9 +263,12 @@ fun MoreOptionsSelectionCard( providerList.forEach { providerInfo -> providerInfo.createOptions.forEach { createOptionInfo -> item { - MoreOptionsInfoRow(providerInfo = providerInfo, + MoreOptionsInfoRow( + providerInfo = providerInfo, createOptionInfo = createOptionInfo, - onOptionSelected = onOptionSelected) + onOptionSelected = { + onOptionSelected(ActiveEntry(providerInfo, createOptionInfo)) + }) } } } @@ -285,7 +287,7 @@ fun MoreOptionsSelectionCard( @Composable fun MoreOptionsRowIntroCard( providerInfo: ProviderInfo, - onDefaultOrNotSelected: (String) -> Unit, + onDefaultOrNotSelected: () -> Unit, ) { Card( backgroundColor = lightBackgroundColor, @@ -302,11 +304,11 @@ fun MoreOptionsRowIntroCard( ) { CancelButton( stringResource(R.string.use_once), - onclick = { onDefaultOrNotSelected(providerInfo.name) } + onclick = onDefaultOrNotSelected ) ConfirmButton( stringResource(R.string.set_as_default), - onclick = { onDefaultOrNotSelected(providerInfo.name) } + onclick = onDefaultOrNotSelected ) } Divider( @@ -388,11 +390,12 @@ fun NavigationButton( @ExperimentalMaterialApi @Composable fun CreationSelectionCard( + requestDisplayInfo: RequestDisplayInfo, providerInfo: ProviderInfo, - onOptionSelected: (Int) -> Unit, + onOptionSelected: () -> Unit, onCancel: () -> Unit, multiProvider: Boolean, - onMoreOptionsSelected: (String) -> Unit, + onMoreOptionsSelected: () -> Unit, ) { Card( backgroundColor = lightBackgroundColor, @@ -427,14 +430,13 @@ fun CreationSelectionCard( LazyColumn( verticalArrangement = Arrangement.spacedBy(2.dp) ) { - providerInfo.createOptions.forEach { item { - CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected) + PrimaryCreateOptionRow(requestDisplayInfo = requestDisplayInfo, + onOptionSelected = onOptionSelected) } - } if (multiProvider) { item { - MoreOptionsRow(onSelect = { onMoreOptionsSelected(providerInfo.name) }) + MoreOptionsRow(onSelect = onMoreOptionsSelected) } } } @@ -494,14 +496,45 @@ fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (Int) @ExperimentalMaterialApi @Composable +fun PrimaryCreateOptionRow( + requestDisplayInfo: RequestDisplayInfo, + onOptionSelected: () -> Unit +) { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = {onOptionSelected()}, + // TODO: Add an icon generated by provider according to requestDisplayInfo type + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Column() { + Text( + text = requestDisplayInfo.userName, + style = Typography.h6, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = requestDisplayInfo.displayName, + style = Typography.body2, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable fun MoreOptionsInfoRow( providerInfo: ProviderInfo, createOptionInfo: CreateOptionInfo, - onOptionSelected: (String) -> Unit + onOptionSelected: () -> Unit ) { Chip( modifier = Modifier.fillMaxWidth(), - onClick = { onOptionSelected(providerInfo.name) }, + onClick = onOptionSelected, leadingIcon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(), diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt index ff44e2ee1b06..38486e2c7d74 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -30,7 +30,8 @@ import com.android.credentialmanager.common.ResultState data class CreatePasskeyUiState( val providers: List<ProviderInfo>, val currentScreenState: CreateScreenState, - val selectedProvider: ProviderInfo? = null, + val requestDisplayInfo: RequestDisplayInfo, + val activeEntry: ActiveEntry? = null, ) class CreatePasskeyViewModel( @@ -56,7 +57,8 @@ class CreatePasskeyViewModel( } else if (uiState.providers.size == 1){ uiState = uiState.copy( currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - selectedProvider = uiState.providers.first() + activeEntry = ActiveEntry(uiState.providers.first(), + uiState.providers.first().createOptions.first()) ) } else { throw java.lang.IllegalStateException("Empty provider list.") @@ -66,14 +68,15 @@ class CreatePasskeyViewModel( fun onProviderSelected(providerName: String) { uiState = uiState.copy( currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - selectedProvider = getProviderInfoByName(providerName) + activeEntry = ActiveEntry(getProviderInfoByName(providerName), + getProviderInfoByName(providerName).createOptions.first()) ) } fun onCreateOptionSelected(createOptionId: Int) { Log.d("Account Selector", "Option selected for creation: $createOptionId") CredentialManagerRepo.getInstance().onOptionSelected( - uiState.selectedProvider!!.name, + uiState.activeEntry?.activeProvider!!.name, createOptionId ) dialogResult.value = DialogResult( @@ -87,24 +90,22 @@ class CreatePasskeyViewModel( } } - fun onMoreOptionsSelected(providerName: String) { + fun onMoreOptionsSelected() { uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - selectedProvider = getProviderInfoByName(providerName) + currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, ) } - fun onBackButtonSelected(providerName: String) { + fun onBackButtonSelected() { uiState = uiState.copy( currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - selectedProvider = getProviderInfoByName(providerName) ) } - fun onMoreOptionsRowSelected(providerName: String) { + fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) { uiState = uiState.copy( currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO, - selectedProvider = getProviderInfoByName(providerName) + activeEntry = activeEntry ) } @@ -113,11 +114,24 @@ class CreatePasskeyViewModel( dialogResult.value = DialogResult(ResultState.CANCELED) } - fun onDefaultOrNotSelected(providerName: String) { + fun onDefaultOrNotSelected() { uiState = uiState.copy( currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - selectedProvider = getProviderInfoByName(providerName) ) // TODO: implement the if choose as default or not logic later } + + fun onPrimaryCreateOptionInfoSelected() { + var createOptionId = uiState.activeEntry?.activeCreateOptionInfo?.id + Log.d("Account Selector", "Option selected for creation: $createOptionId") + if (createOptionId != null) { + CredentialManagerRepo.getInstance().onOptionSelected( + uiState.activeEntry?.activeProvider!!.name, + createOptionId + ) + } + dialogResult.value = DialogResult( + ResultState.COMPLETE, + ) + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt new file mode 100644 index 000000000000..d6f1b5f5c8e9 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.graphics.drawable.Icon + +/** + * UI representation for a credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +abstract class CredentialEntryUi( + val credentialTypeIcon: Icon, + val profileIcon: Icon?, + val lastUsedTimeMillis: Long?, + val note: CharSequence?, +) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + return when (slice.spec?.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> PasskeyCredentialEntryUi.fromSlice(slice) + TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntryUi.fromSlice(slice) + else -> throw IllegalArgumentException("Unexpected type: ${slice.spec?.type}") + } + } + + const val TYPE_PUBLIC_KEY_CREDENTIAL: String = + "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" + const val TYPE_PASSWORD_CREDENTIAL: String = "androidx.credentials.TYPE_PASSWORD" + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt new file mode 100644 index 000000000000..bb3b206500b4 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +class PasskeyCredentialEntryUi( + val userName: CharSequence, + val userDisplayName: CharSequence?, + credentialTypeIcon: Icon, + profileIcon: Icon?, + lastUsedTimeMillis: Long?, + note: CharSequence?, +) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + var userName: CharSequence? = null + var userDisplayName: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var lastUsedTimeMillis: Long? = null + var note: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_NAME)) { + userName = it.text + } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) { + userDisplayName = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } else if (it.hasHint(Entry.HINT_NOTE)) { + note = it.text + } + } + // TODO: fail NPE more elegantly. + return PasskeyCredentialEntryUi( + userName!!, userDisplayName, credentialTypeIcon!!, + profileIcon, lastUsedTimeMillis, note, + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt new file mode 100644 index 000000000000..7311b7081343 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a password credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +class PasswordCredentialEntryUi( + val userName: CharSequence, + val password: CharSequence, + credentialTypeIcon: Icon, + profileIcon: Icon?, + lastUsedTimeMillis: Long?, + note: CharSequence?, +) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + var userName: CharSequence? = null + var password: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var lastUsedTimeMillis: Long? = null + var note: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_NAME)) { + userName = it.text + } else if (it.hasHint(Entry.HINT_PASSWORD_VALUE)) { + password = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } else if (it.hasHint(Entry.HINT_NOTE)) { + note = it.text + } + } + // TODO: fail NPE more elegantly. + return PasswordCredentialEntryUi( + userName!!, password!!, credentialTypeIcon!!, + profileIcon, lastUsedTimeMillis, note, + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt new file mode 100644 index 000000000000..fad3309fb86f --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a save entry used during the create credential flow. + * + * TODO: move to jetpack. + */ +class SaveEntryUi( + val userProviderAccountName: CharSequence, + val credentialTypeIcon: Icon?, + val profileIcon: Icon?, + val passwordCount: Int?, + val passkeyCount: Int?, + val totalCredentialCount: Int?, + val lastUsedTimeMillis: Long?, +) { + companion object { + fun fromSlice(slice: Slice): SaveEntryUi { + var userProviderAccountName: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var passwordCount: Int? = null + var passkeyCount: Int? = null + var totalCredentialCount: Int? = null + var lastUsedTimeMillis: Long? = null + + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) { + userProviderAccountName = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_PASSWORD_COUNT)) { + passwordCount = it.int + } else if (it.hasHint(Entry.HINT_PASSKEY_COUNT)) { + passkeyCount = it.int + } else if (it.hasHint(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) { + totalCredentialCount = it.int + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } + } + // TODO: fail NPE more elegantly. + return SaveEntryUi( + userProviderAccountName!!, credentialTypeIcon, profileIcon, + passwordCount, passkeyCount, totalCredentialCount, lastUsedTimeMillis, + ) + } + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index acb22dac9854..4af25893ea37 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider @@ -66,6 +67,7 @@ object GallerySpaEnvironment : SpaEnvironment() { IllustrationPageProvider, CategoryPageProvider, ActionButtonPageProvider, + ProgressBarPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index e40775a95813..7fd49db93748 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -31,6 +31,7 @@ import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider @@ -54,6 +55,7 @@ object HomePageProvider : SettingsPageProvider { IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt new file mode 100644 index 000000000000..dc45df4a0374 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.page + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.SystemUpdate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPage +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.ProgressBarPreference +import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel +import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.CircularLoadingBar +import com.android.settingslib.spa.widget.ui.CircularProgressBar +import com.android.settingslib.spa.widget.ui.LinearLoadingBar +import kotlinx.coroutines.delay + +private const val TITLE = "Sample ProgressBar" + +object ProgressBarPageProvider : SettingsPageProvider { + override val name = "ProgressBar" + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) + .setIsAllowSearch(true) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } + + @Composable + override fun Page(arguments: Bundle?) { + // Mocks a loading time of 2 seconds. + var loading by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + delay(2000) + loading = false + } + + RegularScaffold(title = TITLE) { + // Auto update the progress and finally jump tp 0.4f. + var progress by remember { mutableStateOf(0f) } + LaunchedEffect(Unit) { + delay(2000) + while (progress < 1f) { + delay(100) + progress += 0.01f + } + delay(500) + progress = 0.4f + } + + // Show as a placeholder for progress bar + LargeProgressBar(progress) + // The remaining information only shows after loading complete. + if (!loading) { + SimpleProgressBar() + ProgressBarWithData() + CircularProgressBar(progress = progress, radius = 160f) + } + } + + // Add loading bar examples, running for 2 seconds. + LinearLoadingBar(isLoading = loading, yOffset = 64.dp) + CircularLoadingBar(isLoading = loading) + } +} + +@Composable +private fun LargeProgressBar(progress: Float) { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Large Progress Bar" + override val progress = progress + override val height = 20f + }) +} + +@Composable +private fun SimpleProgressBar() { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Simple Progress Bar" + override val progress = 0.2f + override val icon = Icons.Outlined.SystemUpdate + }) +} + +@Composable +private fun ProgressBarWithData() { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Progress Bar with Data" + override val progress = 0.2f + override val icon = Icons.Outlined.Delete + }, data = "25G") +} + +@Preview(showBackground = true) +@Composable +private fun ProgressBarPagePreview() { + SettingsTheme { + ProgressBarPageProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 9a34dbf36735..6135203ec703 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -72,7 +72,7 @@ internal fun BaseLayout( } @Composable -private fun BaseIcon( +internal fun BaseIcon( icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt new file mode 100644 index 000000000000..b8c59ad07cfd --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.vector.ImageVector +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.ui.LinearProgressBar +import com.android.settingslib.spa.widget.ui.SettingsTitle + +/** + * The widget model for [ProgressBarPreference] widget. + */ +interface ProgressBarPreferenceModel { + /** + * The title of this [ProgressBarPreference]. + */ + val title: String + + /** + * The progress fraction of the ProgressBar. Should be float in range [0f, 1f] + */ + val progress: Float + + /** + * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default. + */ + val icon: ImageVector? + get() = null + + /** + * The height of the ProgressBar. + */ + val height: Float + get() = 4f + + /** + * Indicates whether to use rounded corner for the progress bars. + */ + val roundedCorner: Boolean + get() = true +} + +/** + * Progress bar preference widget. + * + * Data is provided through [ProgressBarPreferenceModel]. + */ +@Composable +fun ProgressBarPreference(model: ProgressBarPreferenceModel) { + ProgressBarPreference( + title = model.title, + progress = model.progress, + icon = model.icon, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +/** + * Progress bar with data preference widget. + */ +@Composable +fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) { + val icon = model.icon + ProgressBarWithDataPreference( + title = model.title, + data = data, + progress = model.progress, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +@Composable +internal fun ProgressBarPreference( + title: String, + progress: Float, + icon: ImageVector? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + BaseLayout( + title = title, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + ) +} + + +@Composable +internal fun ProgressBarWithDataPreference( + title: String, + data: String, + progress: Float, + icon: (@Composable () -> Unit)? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = SettingsDimension.itemPaddingEnd), + verticalAlignment = Alignment.CenterVertically, + ) { + BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart) + TitleWithData( + title = title, + data = data, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + modifier = Modifier + .weight(1f) + .padding(vertical = SettingsDimension.itemPaddingVertical), + ) + } +} + +@Composable +private fun TitleWithData( + title: String, + data: String, + subTitle: @Composable () -> Unit, + modifier: Modifier +) { + Column(modifier) { + Row { + Box(modifier = Modifier.weight(1f)) { + SettingsTitle(title) + } + Text( + text = data, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium, + ) + } + subTitle() + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt new file mode 100644 index 000000000000..1741f134f3d1 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Indeterminate linear progress bar. Expresses an unspecified wait time. + */ +@Composable +fun LinearLoadingBar( + isLoading: Boolean, + xOffset: Dp = 0.dp, + yOffset: Dp = 0.dp +) { + if (isLoading) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .absoluteOffset(xOffset, yOffset) + ) + } +} + +/** + * Indeterminate circular progress bar. Expresses an unspecified wait time. + */ +@Composable +fun CircularLoadingBar(isLoading: Boolean) { + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt new file mode 100644 index 000000000000..5d8502db4986 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.dp + +/** + * Determinate linear progress bar. Displays the current progress of the whole process. + * + * Rounded corner is supported and enabled by default. + */ +@Composable +fun LinearProgressBar( + progress: Float, + height: Float = 4f, + roundedCorner: Boolean = true +) { + Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) { + val color = MaterialTheme.colorScheme.onSurface + val trackColor = MaterialTheme.colorScheme.surfaceVariant + Canvas( + Modifier + .progressSemantics(progress) + .fillMaxWidth() + .height(height.dp) + ) { + drawLinearBarTrack(trackColor, roundedCorner) + drawLinearBar(progress, color, roundedCorner) + } + } +} + +private fun DrawScope.drawLinearBar( + endFraction: Float, + color: Color, + roundedCorner: Boolean +) { + val width = endFraction * size.width + drawRoundRect( + color = color, + size = Size(width, size.height), + cornerRadius = if (roundedCorner) CornerRadius( + size.height / 2, + size.height / 2 + ) else CornerRadius.Zero, + ) +} + +private fun DrawScope.drawLinearBarTrack( + color: Color, + roundedCorner: Boolean +) = drawLinearBar(1f, color, roundedCorner) + +/** + * Determinate circular progress bar. Displays the current progress of the whole process. + * + * Displayed in default material3 style, and rounded corner is not supported. + */ +@Composable +fun CircularProgressBar(progress: Float, radius: Float = 40f) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = progress, + modifier = Modifier.size(radius.dp, radius.dp) + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt new file mode 100644 index 000000000000..5611f8ce14db --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ProgressBarPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + } + + @Test + fun data_displayed() { + composeTestRule.setContent { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }, data = "Data") + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + composeTestRule.onNodeWithText("Data").assertIsDisplayed() + } + + @Test + fun progressBar_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + + fun progressEqualsTo(progress: Float): SemanticsMatcher = + SemanticsMatcher.expectValue( + ProgressBarRangeInfo, + ProgressBarRangeInfo(progress, 0f..1f, 0) + ) + composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed() + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt index 7ae11758a0ce..3e5dd527a6b3 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt @@ -16,6 +16,9 @@ package com.android.settingslib.spa.widget.preference +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText @@ -41,5 +44,20 @@ class SliderPreferenceTest { composeTestRule.onNodeWithText("Slider").assertIsDisplayed() } - // TODO: Add more unit tests for SliderPreference widget. + @Test + fun slider_displayed() { + composeTestRule.setContent { + SliderPreference(object : SliderPreferenceModel { + override val title = "Slider" + override val initValue = 40 + }) + } + + fun progressEqualsTo(progress: Float): SemanticsMatcher = + SemanticsMatcher.expectValue( + ProgressBarRangeInfo, + ProgressBarRangeInfo(progress, 0f..100f, 0) + ) + composeTestRule.onNode(progressEqualsTo(40f)).assertIsDisplayed() + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java index 91b852ab9f67..6641db1e6449 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java @@ -235,7 +235,7 @@ public class A2dpProfile implements LocalBluetoothProfile { /** * @return whether high quality audio is enabled or not */ - @RequiresApi(Build.VERSION_CODES.TIRAMISU) + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public boolean isHighQualityAudioEnabled(BluetoothDevice device) { BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice(); if (bluetoothDevice == null) { @@ -287,7 +287,7 @@ public class A2dpProfile implements LocalBluetoothProfile { * @param device to get codec label from * @return the label associated with the device codec */ - @RequiresApi(Build.VERSION_CODES.TIRAMISU) + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public String getHighQualityAudioOptionLabel(BluetoothDevice device) { BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice(); int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec; diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index 7275d6be99ad..1745379c3034 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER; import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.ParentalControlsUtilsInternal; @@ -45,6 +46,8 @@ public final class ActionDisabledByAdminControllerFactory { return new BiometricActionDisabledByAdminController(stringProvider); } else if (isFinancedDevice(context)) { return new FinancedDeviceActionDisabledByAdminController(stringProvider); + } else if (isSupervisedDevice(context)) { + return new SupervisedDeviceActionDisabledByAdminController(stringProvider, restriction); } else { return new ManagedDeviceActionDisabledByAdminController( stringProvider, @@ -54,6 +57,15 @@ public final class ActionDisabledByAdminControllerFactory { } } + private static boolean isSupervisedDevice(Context context) { + DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + ComponentName supervisionComponent = + devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( + new UserHandle(UserHandle.myUserId())); + return supervisionComponent != null; + } + /** * @return true if the restriction == UserManager.DISALLOW_BIOMETRIC and parental consent * is required. diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java index 6e93494bf40e..714accc09763 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.net.Uri; import android.provider.Settings; import android.util.Log; @@ -60,6 +61,10 @@ public class BiometricActionDisabledByAdminController extends BaseActionDisabled final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING) .putExtra(Settings.EXTRA_SUPERVISOR_RESTRICTED_SETTING_KEY, Settings.SUPERVISOR_VERIFICATION_SETTING_BIOMETRICS) + .setData(new Uri.Builder() + .scheme("policy") + .appendPath("biometric") + .build()) .setPackage(enforcedAdmin.component.getPackageName()); context.startActivity(intent); }; diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java index b83837e6caf6..7ff91f8526fb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java @@ -79,6 +79,11 @@ public interface DeviceAdminStringProvider { String getDisabledBiometricsParentConsentTitle(); /** + * Returns the dialog title when the setting is blocked by supervision app. + */ + String getDisabledByParentContent(); + + /** * Returns the dialog contents for when biometrics require parental consent. */ String getDisabledBiometricsParentConsentContent(); diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java new file mode 100644 index 000000000000..815293e9c2a8 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.enterprise; + +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.util.Log; + +import com.android.settingslib.RestrictedLockUtils; + +import org.jetbrains.annotations.Nullable; + +final class SupervisedDeviceActionDisabledByAdminController + extends BaseActionDisabledByAdminController { + private static final String TAG = "SupervisedDeviceActionDisabledByAdminController"; + private final String mRestriction; + + SupervisedDeviceActionDisabledByAdminController( + DeviceAdminStringProvider stringProvider, String restriction) { + super(stringProvider); + mRestriction = restriction; + } + + @Override + public void setupLearnMoreButton(Context context) { + + } + + @Override + public String getAdminSupportTitle(@Nullable String restriction) { + return mStringProvider.getDisabledBiometricsParentConsentTitle(); + } + + @Override + public CharSequence getAdminSupportContentString(Context context, + @Nullable CharSequence supportMessage) { + return mStringProvider.getDisabledByParentContent(); + } + + @Nullable + @Override + public DialogInterface.OnClickListener getPositiveButtonListener(Context context, + RestrictedLockUtils.EnforcedAdmin enforcedAdmin) { + final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING) + .setData(new Uri.Builder() + .scheme("policy") + .appendPath("user_restrictions") + .appendPath(mRestriction) + .build()) + .setPackage(enforcedAdmin.component.getPackageName()); + ComponentName resolvedSupervisionActivity = + intent.resolveActivity(context.getPackageManager()); + if (resolvedSupervisionActivity == null) { + return null; + } + return (dialog, which) -> { + Log.d(TAG, "Positive button clicked, component: " + enforcedAdmin.component); + context.startActivity(intent); + }; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java index 39977dfa5c80..f969a63dc663 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java @@ -41,19 +41,19 @@ public class MobileNetworkTypeIconsTest { MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G); - assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name); + assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name); assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G); } @Test public void getNetworkTypeIcon_unknown_returnsUnknown() { - SignalIcon.MobileIconGroup unknownGroup = - new SignalIcon.MobileIconGroup("testUnknownNameHere", 45, 6); + SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup( + "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6); MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup); assertThat(icon.getName()).isEqualTo("testUnknownNameHere"); - assertThat(icon.getIconResId()).isEqualTo(45); - assertThat(icon.getContentDescriptionResId()).isEqualTo(6); + assertThat(icon.getIconResId()).isEqualTo(6); + assertThat(icon.getContentDescriptionResId()).isEqualTo(45); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java index 99e13c325472..1d5f1b20e31e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java @@ -32,6 +32,7 @@ class FakeDeviceAdminStringProvider implements DeviceAdminStringProvider { "default_disabled_by_policy_title_financed_device"; static final String DEFAULT_BIOMETRIC_TITLE = "biometric_title"; static final String DEFAULT_BIOMETRIC_CONTENTS = "biometric_contents"; + static final String DISABLED_BY_PARENT_CONTENT = "disabled_by_parent_constent"; static final DeviceAdminStringProvider DEFAULT_DEVICE_ADMIN_STRING_PROVIDER = new FakeDeviceAdminStringProvider(/* url = */ null); @@ -97,6 +98,11 @@ class FakeDeviceAdminStringProvider implements DeviceAdminStringProvider { } @Override + public String getDisabledByParentContent() { + return DISABLED_BY_PARENT_CONTENT; + } + + @Override public String getDisabledBiometricsParentConsentContent() { return DEFAULT_BIOMETRIC_CONTENTS; } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java new file mode 100644 index 000000000000..5d249c7f936c --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java @@ -0,0 +1,97 @@ +/* + * 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.settingslib.enterprise; + +import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ADMIN_COMPONENT; +import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN; +import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID; +import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DEVICE_ADMIN_STRING_PROVIDER; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.TestCase.assertEquals; + +import static org.mockito.Mockito.mock; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.UserManager; +import android.provider.Settings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowResolveInfo; + +@RunWith(RobolectricTestRunner.class) +public class SupervisedDeviceActionDisabledByAdminControllerTest { + + private Context mContext; + + private ActionDisabledByAdminControllerTestUtils mTestUtils; + private SupervisedDeviceActionDisabledByAdminController mController; + + @Before + public void setUp() { + mContext = Robolectric.buildActivity(Activity.class).setup().get(); + + mTestUtils = new ActionDisabledByAdminControllerTestUtils(); + + mController = new SupervisedDeviceActionDisabledByAdminController( + DEFAULT_DEVICE_ADMIN_STRING_PROVIDER, UserManager.DISALLOW_ADD_USER); + mController.initialize(mTestUtils.createLearnMoreButtonLauncher()); + mController.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID); + } + + @Test + public void buttonClicked() { + Uri restrictionUri = Uri.parse("policy:/user_restrictions/no_add_user"); + Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING) + .setData(restrictionUri) + .setPackage(ADMIN_COMPONENT.getPackageName()); + ResolveInfo resolveInfo = ShadowResolveInfo.newResolveInfo("Admin Activity", + ADMIN_COMPONENT.getPackageName(), "InfoActivity"); + shadowOf(mContext.getPackageManager()).addResolveInfoForIntent(intent, resolveInfo); + + DialogInterface.OnClickListener listener = + mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN); + assertNotNull("Supervision controller must supply a non-null listener", listener); + listener.onClick(mock(DialogInterface.class), 0 /* which */); + + Intent nextIntent = shadowOf(RuntimeEnvironment.application).getNextStartedActivity(); + assertEquals(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING, + nextIntent.getAction()); + assertEquals(restrictionUri, nextIntent.getData()); + assertEquals(ADMIN_COMPONENT.getPackageName(), nextIntent.getPackage()); + } + + @Test + public void noButton() { + // No supervisor restricted setting Activity + DialogInterface.OnClickListener listener = + mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN); + assertNull("Supervision controller generates null listener", listener); + } +} diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 3623c7821dfe..edea3abc0618 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -311,4 +311,7 @@ <!-- Whether tilt to bright is enabled by default. --> <bool name="def_wearable_tiltToBrightEnabled">false</bool> + + <!-- Whether vibrate icon is shown in the status bar by default. --> + <integer name="def_statusBarVibrateIconEnabled">0</integer> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 3a25d85b5ecf..ccbfac226c46 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3659,7 +3659,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 210; + private static final int SETTINGS_VERSION = 211; private final int mUserId; @@ -5531,7 +5531,17 @@ public class SettingsProvider extends ContentProvider { // removed now that feature is enabled for everyone currentVersion = 210; } - + if (currentVersion == 210) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + currentVersion = 211; + } // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9747a6c5ca70..aea2f5235201 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -817,7 +817,8 @@ public class SettingsBackupTest { Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT, Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, - Settings.Secure.UI_TRANSLATION_ENABLED); + Settings.Secure.UI_TRANSLATION_ENABLED, + Settings.Secure.CREDENTIAL_SERVICE); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index fecf1241acf8..90fab08ed43e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -154,6 +154,7 @@ <uses-permission android:name="android.permission.CONTROL_UI_TRACING" /> <uses-permission android:name="android.permission.SIGNAL_PERSISTENT_PROCESSES" /> <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /> + <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" /> <!-- Internal permissions granted to the shell. --> <uses-permission android:name="android.permission.FORCE_BACK" /> <uses-permission android:name="android.permission.BATTERY_STATS" /> @@ -210,6 +211,7 @@ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.CREATE_USERS" /> + <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" /> <uses-permission android:name="android.permission.QUERY_USERS" /> <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" /> <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" /> @@ -715,6 +717,9 @@ <!-- Permission required for CTS test - ActivityPermissionRationaleTest --> <uses-permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY" /> + <!-- Permission required for CTS test - CtsDeviceLockTestCases --> + <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index ca36fa43da76..fdfad2bc2fa1 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.Looper import android.util.Log import android.util.MathUtils -import android.view.GhostView import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -86,6 +85,9 @@ constructor( */ val sourceIdentity: Any + /** The CUJ associated to this controller. */ + val cuj: DialogCuj? + /** * Move the drawing of the source in the overlay of [viewGroup]. * @@ -142,7 +144,31 @@ constructor( * controlled by this controller. */ // TODO(b/252723237): Make this non-nullable - fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder? + fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? + + companion object { + /** + * Create a [Controller] that can animate [source] to and from a dialog. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + fun fromView(source: View, cuj: DialogCuj? = null): Controller? { + if (source.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $source is not attached to a ViewGroup", + Exception(), + ) + return null + } + + return ViewDialogLaunchAnimatorController(source, cuj) + } + } } /** @@ -172,7 +198,12 @@ constructor( cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { - show(dialog, createController(view), cuj, animateBackgroundBoundsChange) + val controller = Controller.fromView(view, cuj) + if (controller == null) { + dialog.show() + } else { + show(dialog, controller, animateBackgroundBoundsChange) + } } /** @@ -187,10 +218,10 @@ constructor( * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. */ + @JvmOverloads fun show( dialog: Dialog, controller: Controller, - cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { if (Looper.myLooper() != Looper.getMainLooper()) { @@ -207,7 +238,10 @@ constructor( it.dialog.window.decorView.viewRootImpl == controller.viewRoot } val animateFrom = - animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller + animatedParent?.dialogContentWithBackground?.let { + Controller.fromView(it, controller.cuj) + } + ?: controller if (animatedParent == null && animateFrom !is LaunchableView) { // Make sure the View we launch from implements LaunchableView to avoid visibility @@ -244,96 +278,12 @@ constructor( animateBackgroundBoundsChange, animatedParent, isForTesting, - cuj, ) openedDialogs.add(animatedDialog) animatedDialog.start() } - /** Create a [Controller] that can animate [source] to & from a dialog. */ - private fun createController(source: View): Controller { - return object : Controller { - override val viewRoot: ViewRootImpl - get() = source.viewRootImpl - - override val sourceIdentity: Any = source - - override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { - // Create a temporary ghost of the source (which will make it invisible) and add it - // to the host dialog. - GhostView.addGhost(source, viewGroup) - - // The ghost of the source was just created, so the source is currently invisible. - // We need to make sure that it stays invisible as long as the dialog is shown or - // animating. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) - } - - override fun stopDrawingInOverlay() { - // Note: here we should remove the ghost from the overlay, but in practice this is - // already done by the launch controllers created below. - - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - source.visibility = View.VISIBLE - } - - override fun createLaunchController(): LaunchAnimator.Controller { - val delegate = GhostedViewLaunchAnimatorController(source) - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another - // ghost (that ghosts only the source content, and not its background) will - // be added right after this by the delegate and will be animated. - GhostView.removeGhost(source) - delegate.onLaunchAnimationStart(isExpandingFullyAbove) - } - - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) - - // We hide the source when the dialog is showing. We will make this view - // visible again when dismissing the dialog. This does nothing if the source - // implements [LaunchableView], as it's already INVISIBLE in that case. - source.visibility = View.INVISIBLE - } - } - } - - override fun createExitController(): LaunchAnimator.Controller { - return GhostedViewLaunchAnimatorController(source) - } - - override fun shouldAnimateExit(): Boolean { - // The source should be invisible by now, if it's not then something else changed - // its visibility and we probably don't want to run the animation. - if (source.visibility != View.INVISIBLE) { - return false - } - - return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) - } - - override fun onExitAnimationCancelled() { - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - - // If the view is invisible it's probably because of us, so we make it visible - // again. - if (source.visibility == View.INVISIBLE) { - source.visibility = View.VISIBLE - } - } - - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { - return InteractionJankMonitor.Configuration.Builder.withView(cuj, source) - } - } - } - /** * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will * allow for dismissing the whole stack. @@ -563,9 +513,6 @@ private class AnimatedDialog( * Whether synchronization should be disabled, which can be useful if we are running in a test. */ private val forceDisableSynchronization: Boolean, - - /** Interaction to which the dialog animation is associated. */ - private val cuj: DialogCuj? = null ) { /** * The DecorView of this dialog window. @@ -618,8 +565,9 @@ private class AnimatedDialog( private var hasInstrumentedJank = false fun start() { + val cuj = controller.cuj if (cuj != null) { - val config = controller.jankConfigurationBuilder(cuj.cujType) + val config = controller.jankConfigurationBuilder() if (config != null) { if (cuj.tag != null) { config.setTag(cuj.tag) @@ -917,7 +865,7 @@ private class AnimatedDialog( } if (hasInstrumentedJank) { - interactionJankMonitor.end(cuj!!.cujType) + interactionJankMonitor.end(controller.cuj!!.cujType) } } ) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index 8ce372dbb278..40a5e9794d37 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -30,7 +30,12 @@ interface Expandable { */ fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller? - // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here. + /** + * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into + * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + */ + fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller? companion object { /** @@ -39,6 +44,7 @@ interface Expandable { * Note: The background of [view] should be a (rounded) rectangle so that it can be properly * animated. */ + @JvmStatic fun fromView(view: View): Expandable { return object : Expandable { override fun activityLaunchController( @@ -46,6 +52,12 @@ interface Expandable { ): ActivityLaunchAnimator.Controller? { return ActivityLaunchAnimator.Controller.fromView(view, cujType) } + + override fun dialogLaunchController( + cuj: DialogCuj? + ): DialogLaunchAnimator.Controller? { + return DialogLaunchAnimator.Controller.fromView(view, cuj) + } } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt new file mode 100644 index 000000000000..ecee598afe4e --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.view.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import com.android.internal.jank.InteractionJankMonitor + +/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */ +class ViewDialogLaunchAnimatorController +internal constructor( + private val source: View, + override val cuj: DialogCuj?, +) : DialogLaunchAnimator.Controller { + override val viewRoot: ViewRootImpl + get() = source.viewRootImpl + + override val sourceIdentity: Any = source + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Create a temporary ghost of the source (which will make it invisible) and add it + // to the host dialog. + GhostView.addGhost(source, viewGroup) + + // The ghost of the source was just created, so the source is currently invisible. + // We need to make sure that it stays invisible as long as the dialog is shown or + // animating. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + } + + override fun stopDrawingInOverlay() { + // Note: here we should remove the ghost from the overlay, but in practice this is + // already done by the launch controllers created below. + + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + source.visibility = View.VISIBLE + } + + override fun createLaunchController(): LaunchAnimator.Controller { + val delegate = GhostedViewLaunchAnimatorController(source) + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another + // ghost (that ghosts only the source content, and not its background) will + // be added right after this by the delegate and will be animated. + GhostView.removeGhost(source) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + + // We hide the source when the dialog is showing. We will make this view + // visible again when dismissing the dialog. This does nothing if the source + // implements [LaunchableView], as it's already INVISIBLE in that case. + source.visibility = View.INVISIBLE + } + } + } + + override fun createExitController(): LaunchAnimator.Controller { + return GhostedViewLaunchAnimatorController(source) + } + + override fun shouldAnimateExit(): Boolean { + // The source should be invisible by now, if it's not then something else changed + // its visibility and we probably don't want to run the animation. + if (source.visibility != View.INVISIBLE) { + return false + } + + return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) + } + + override fun onExitAnimationCancelled() { + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + + // If the view is invisible it's probably because of us, so we make it visible + // again. + if (source.visibility == View.INVISIBLE) { + source.visibility = View.VISIBLE + } + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + val type = cuj?.cujType ?: return null + return InteractionJankMonitor.Configuration.Builder.withView(type, source) + } +} diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 9671adde4904..40580d29380b 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -47,6 +47,10 @@ java_test_host { "tests/**/*.kt", "tests/**/*.java", ], + data: [ + ":framework", + ":androidx.annotation_annotation", + ], static_libs: [ "SystemUILintChecker", "junit", diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index 4eeeb850292a..4b9aa13c0240 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -32,7 +32,8 @@ import org.jetbrains.uast.UReferenceExpression class SoftwareBitmapDetector : Detector(), SourceCodeScanner { override fun getApplicableReferenceNames(): List<String> { - return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + return mutableListOf( + "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102") } override fun visitReference( @@ -40,13 +41,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { reference: UReferenceExpression, referenced: PsiElement ) { - val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( ISSUE, referenced, - context.getNameLocation(referenced), + context.getNameLocation(reference), "Replace software bitmap with `Config.HARDWARE`" ) } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt index d4c55c0d9149..141dd0535986 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -18,185 +18,22 @@ package com.android.internal.systemui.lint import com.android.annotations.NonNull import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile +import java.io.File import org.intellij.lang.annotations.Language @Suppress("UnstableApiUsage") @NonNull private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented() -internal val commonSettingsCode = - """ -public static float getFloat(ContentResolver cr, String name) { return 0.0f; } -public static long getLong(ContentResolver cr, String name) { - return 0L; -} -public static int getInt(ContentResolver cr, String name) { - return 0; -} -public static String getString(ContentResolver cr, String name) { - return ""; -} -public static float getFloat(ContentResolver cr, String name, float def) { - return 0.0f; -} -public static long getLong(ContentResolver cr, String name, long def) { - return 0L; -} -public static int getInt(ContentResolver cr, String name, int def) { - return 0; -} -public static String getString(ContentResolver cr, String name, String def) { - return ""; -} -public static boolean putFloat(ContentResolver cr, String name, float value) { - return true; -} -public static boolean putLong(ContentResolver cr, String name, long value) { - return true; -} -public static boolean putInt(ContentResolver cr, String name, int value) { - return true; -} -public static boolean putFloat(ContentResolver cr, String name) { - return true; -} -public static boolean putString(ContentResolver cr, String name, String value) { - return true; -} -""" - /* * This file contains stubs of framework APIs and System UI classes for testing purposes only. The * stubs are not used in the lint detectors themselves. */ internal val androidStubs = arrayOf( - indentedJava( - """ -package android.app; - -public class ActivityManager { - public static int getCurrentUser() {} -} -""" - ), - indentedJava( - """ -package android.accounts; - -public class AccountManager { - public static AccountManager get(Context context) { return null; } -} -""" - ), - indentedJava( - """ -package android.os; -import android.content.pm.UserInfo; -import android.annotation.UserIdInt; - -public class UserManager { - public UserInfo getUserInfo(@UserIdInt int userId) {} -} -""" - ), - indentedJava(""" -package android.annotation; - -public @interface UserIdInt {} -"""), - indentedJava(""" -package android.content.pm; - -public class UserInfo {} -"""), - indentedJava(""" -package android.os; - -public class Looper {} -"""), - indentedJava(""" -package android.os; - -public class Handler {} -"""), - indentedJava(""" -package android.content; - -public class ServiceConnection {} -"""), - indentedJava(""" -package android.os; - -public enum UserHandle { - ALL -} -"""), - indentedJava( - """ -package android.content; -import android.os.UserHandle; -import android.os.Handler; -import android.os.Looper; -import java.util.concurrent.Executor; - -public class Context { - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {} - public void registerReceiverAsUser( - BroadcastReceiver receiver, UserHandle user, IntentFilter filter, - String broadcastPermission, Handler scheduler) {} - public void registerReceiverForAllUsers( - BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, - Handler scheduler) {} - public void sendBroadcast(Intent intent) {} - public void sendBroadcast(Intent intent, String receiverPermission) {} - public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {} - public void bindService(Intent intent) {} - public void bindServiceAsUser( - Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {} - public void unbindService(ServiceConnection connection) {} - public Looper getMainLooper() { return null; } - public Executor getMainExecutor() { return null; } - public Handler getMainThreadHandler() { return null; } - public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; } - public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name); -} -""" - ), - indentedJava( - """ -package android.app; -import android.content.Context; - -public class Activity extends Context {} -""" - ), - indentedJava( - """ -package android.graphics; - -public class Bitmap { - public enum Config { - ARGB_8888, - RGB_565, - HARDWARE - } - public static Bitmap createBitmap(int width, int height, Config config) { - return null; - } -} -""" - ), - indentedJava(""" -package android.content; - -public class BroadcastReceiver {} -"""), - indentedJava(""" -package android.content; - -public class IntentFilter {} -"""), + LibraryReferenceTestFile(File("framework.jar").canonicalFile), + LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile), indentedJava( """ package com.android.systemui.settings; @@ -208,47 +45,4 @@ public interface UserTracker { } """ ), - indentedJava( - """ -package androidx.annotation; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.CONSTRUCTOR; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface WorkerThread { -} -""" - ), - indentedJava( - """ -package android.provider; - -public class Settings { - public static final class Global { - public static final String UNLOCK_SOUND = "unlock_sound"; - """ + - commonSettingsCode + - """ - } - public static final class Secure { - """ + - commonSettingsCode + - """ - } - public static final class System { - """ + - commonSettingsCode + - """ - } -} -""" - ), ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index 090ddf88fa3c..c632636eb9c8 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -51,12 +51,12 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { .run() .expect( """ - src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - ARGB_8888, - ~~~~~~~~~ - src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - RGB_565, - ~~~~~~~ + src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + ~~~~~~~ + src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + ~~~~~~~~~ 0 errors, 2 warnings """ ) @@ -67,7 +67,7 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { lint() .files( TestFiles.java( - """ + """ import android.graphics.Bitmap; public class TestClass { @@ -76,8 +76,7 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { } } """ - ) - .indented(), + ), *stubs ) .issues(SoftwareBitmapDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt index 2183b3805eed..3f93f075fe8b 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt @@ -3,9 +3,42 @@ package com.android.internal.systemui.lint import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestLintTask import java.io.File +import org.junit.ClassRule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement @Suppress("UnstableApiUsage") +@RunWith(JUnit4::class) abstract class SystemUILintDetectorTest : LintDetectorTest() { + + companion object { + @ClassRule + @JvmField + val libraryChecker: LibraryExists = + LibraryExists("framework.jar", "androidx.annotation_annotation.jar") + } + + class LibraryExists(vararg val libraryNames: String) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + for (libName in libraryNames) { + val libFile = File(libName) + if (!libFile.canonicalFile.exists()) { + throw Exception( + "Could not find $libName in the test's working directory. " + + "File ${libFile.absolutePath} does not exist." + ) + } + } + base.evaluate() + } + } + } + } /** * Customize the lint task to disable SDK usage completely. This ensures that running the tests * in Android Studio has the same result as running the tests in atest diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt index 065c3149c2f5..50c3d7e1e76b 100644 --- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt @@ -40,17 +40,16 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.animation.LaunchAnimator import kotlin.math.roundToInt -/** A controller that can control animated launches. */ +/** A controller that can control animated launches from an [Expandable]. */ interface ExpandableController { - /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */ - fun forActivity(): ActivityLaunchAnimator.Controller - - /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */ - fun forDialog(): DialogLaunchAnimator.Controller + /** The [Expandable] controlled by this controller. */ + val expandable: Expandable } /** @@ -120,13 +119,26 @@ internal class ExpandableControllerImpl( private val layoutDirection: LayoutDirection, private val isComposed: State<Boolean>, ) : ExpandableController { - override fun forActivity(): ActivityLaunchAnimator.Controller { - return activityController() - } + override val expandable: Expandable = + object : Expandable { + override fun activityLaunchController( + cujType: Int?, + ): ActivityLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } - override fun forDialog(): DialogLaunchAnimator.Controller { - return dialogController() - } + return activityController(cujType) + } + + override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } + + return dialogController(cuj) + } + } /** * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog @@ -233,7 +245,7 @@ internal class ExpandableControllerImpl( } /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */ - private fun activityController(): ActivityLaunchAnimator.Controller { + private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller { val delegate = launchController() return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -248,10 +260,11 @@ internal class ExpandableControllerImpl( } } - private fun dialogController(): DialogLaunchAnimator.Controller { + private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller { return object : DialogLaunchAnimator.Controller { override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl override val sourceIdentity: Any = this@ExpandableControllerImpl + override val cuj: DialogCuj? = cuj override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { val newOverlay = viewGroup.overlay as ViewGroupOverlay @@ -294,9 +307,7 @@ internal class ExpandableControllerImpl( isDialogShowing.value = false } - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { // TODO(b/252723237): Add support for jank monitoring when animating from a // Composable. return null diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index d0d3052bc544..31ab24748c93 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -832,7 +832,6 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index b3dd95553ed0..dee0f5cd1979 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -205,6 +205,13 @@ enum class Style(internal val coreSpec: CoreSpec) { n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) )), + MONOCHROMATIC(CoreSpec( + a1 = TonalSpec(HueSource(), ChromaConstant(.0)), + a2 = TonalSpec(HueSource(), ChromaConstant(.0)), + a3 = TonalSpec(HueSource(), ChromaConstant(.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(.0)) + )), } class ColorScheme( @@ -219,7 +226,7 @@ class ColorScheme( val neutral1: List<Int> val neutral2: List<Int> - constructor(@ColorInt seed: Int, darkTheme: Boolean): + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads @@ -227,7 +234,7 @@ class ColorScheme( wallpaperColors: WallpaperColors, darkTheme: Boolean, style: Style = Style.TONAL_SPOT - ): + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allAccentColors: List<Int> @@ -472,4 +479,4 @@ class ColorScheme( return huePopulation } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml new file mode 100644 index 000000000000..de0e526a97c3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/> + <solid android:color="@color/user_switcher_fullscreen_bg"/> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 46f6ab2399d1..0a55cf779683 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -119,6 +119,7 @@ <dimen name="bouncer_user_switcher_width">248dp</dimen> <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen> <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen> + <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen> <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index bc8e540cb612..3bcc37a478c9 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -16,45 +16,47 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" + style="?headerStyle" android:layout_width="wrap_content" android:layout_height="match_parent"> <ImageView android:id="@+id/icon" - style="@style/TextAppearance.AuthNonBioCredential.Icon" + style="?headerIconStyle" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:contentDescription="@null"/> <TextView android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" + style="?titleTextAppearance" android:layout_below="@id/icon" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" + style="?subTitleTextAppearance" android:layout_below="@id/title" android:layout_alignParentLeft="true" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" + style="?descriptionTextAppearance" android:layout_below="@id/subtitle" android:layout_alignParentLeft="true" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout> @@ -67,7 +69,7 @@ <ImeAwareEditText android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" + style="?passwordTextAppearance" android:layout_width="208dp" android:layout_height="wrap_content" android:layout_gravity="center" @@ -77,7 +79,7 @@ <TextView android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" + style="?errorTextAppearance" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" /> diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index 19a85fec1397..a3dd334bd667 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -16,91 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> - <LinearLayout + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center" - android:orientation="vertical"> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + android:layout_weight="1"> <ImageView android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> <TextView android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + </RelativeLayout> + + <FrameLayout + android:layout_weight="1" + style="?containerStyle" + android:layout_width="0dp" + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent"/> <TextView android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> - - </LinearLayout> - - <LinearLayout - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:orientation="vertical" - android:gravity="center" - android:paddingLeft="0dp" - android:paddingRight="0dp" - android:paddingTop="0dp" - android:paddingBottom="16dp" - android:clipToPadding="false"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - style="@style/LockPatternContainerStyle"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - style="@style/LockPatternStyleBiometricPrompt"/> - - </FrameLayout> + android:layout_gravity="center_horizontal|bottom"/> - </LinearLayout> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 75a80bc39a1f..774b335f913e 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -16,43 +16,45 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:elevation="@dimen/biometric_dialog_elevation" - android:orientation="vertical"> + android:orientation="vertical" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" + style="?headerStyle" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/icon" - style="@style/TextAppearance.AuthNonBioCredential.Icon" + style="?headerIconStyle" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:contentDescription="@null"/> <TextView android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" + style="?titleTextAppearance" android:layout_below="@id/icon" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" + style="?subTitleTextAppearance" android:layout_below="@id/title" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" + style="?descriptionTextAppearance" android:layout_below="@id/subtitle" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> </RelativeLayout> @@ -64,7 +66,7 @@ <ImeAwareEditText android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" + style="?passwordTextAppearance" android:layout_width="208dp" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" @@ -74,7 +76,7 @@ <TextView android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" + style="?errorTextAppearance" android:layout_gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index dada9813c320..4af997017bba 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -16,87 +16,66 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> - - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> + + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </RelativeLayout> - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </LinearLayout> + <FrameLayout + android:id="@+id/auth_credential_container" + style="?containerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <LinearLayout + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/auth_credential_header" - android:gravity="center" - android:orientation="vertical" - android:paddingBottom="16dp" - android:paddingTop="60dp"> + android:layout_height="match_parent"/> - <FrameLayout - style="@style/LockPatternContainerStyle" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - style="@style/LockPatternStyle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" /> - - </FrameLayout> - - </LinearLayout> - - <LinearLayout + <TextView + android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true"> - - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - - </LinearLayout> - - </RelativeLayout> + android:layout_gravity="center_horizontal|bottom"/> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 4da77118f00b..bc97e511e7f4 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -19,12 +19,12 @@ <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/media_ttt_sender_chip" + android:id="@+id/chipbar_root_view" android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout - android:id="@+id/media_ttt_sender_chip_inner" + android:id="@+id/chipbar_inner" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -39,7 +39,7 @@ > <com.android.internal.widget.CachingIconView - android:id="@+id/app_icon" + android:id="@+id/start_icon" android:layout_width="@dimen/media_ttt_app_icon_size" android:layout_height="@dimen/media_ttt_app_icon_size" android:layout_marginEnd="12dp" @@ -69,7 +69,7 @@ /> <ImageView - android:id="@+id/failure_icon" + android:id="@+id/error" android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_height="@dimen/media_ttt_status_icon_size" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" @@ -78,11 +78,11 @@ android:alpha="0.0" /> + <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. --> <TextView - android:id="@+id/undo" + android:id="@+id/end_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/media_transfer_undo" android:textColor="?androidprv:attr/textColorOnAccent" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:textSize="@dimen/media_ttt_text_size" diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index ac9a947f4417..aefd9981d02e 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -24,7 +24,36 @@ <item name="android:paddingEnd">24dp</item> <item name="android:paddingTop">48dp</item> <item name="android:paddingBottom">10dp</item> - <item name="android:gravity">top|center_horizontal</item> + <item name="android:gravity">top|left</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">320dp</item> + <item name="android:maxWidth">320dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">60dp</item> + <item name="android:paddingVertical">20dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> </style> </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml new file mode 100644 index 000000000000..8148d3dfaf7d --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> +</resources> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml new file mode 100644 index 000000000000..771de08cb360 --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">180dp</item> + <item name="android:paddingVertical">80dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml new file mode 100644 index 000000000000..f9ed67d89de7 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml new file mode 100644 index 000000000000..78d299c483e6 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">240dp</item> + <item name="android:paddingVertical">120dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 9a71995383ac..df0659d67afe 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -191,5 +191,18 @@ <declare-styleable name="DelayableMarqueeTextView"> <attr name="marqueeDelay" format="integer" /> </declare-styleable> + + <declare-styleable name="AuthCredentialView"> + <attr name="lockPatternStyle" format="reference" /> + <attr name="lockPinPasswordStyle" format="reference" /> + <attr name="containerStyle" format="reference" /> + <attr name="headerStyle" format="reference" /> + <attr name="headerIconStyle" format="reference" /> + <attr name="titleTextAppearance" format="reference" /> + <attr name="subTitleTextAppearance" format="reference" /> + <attr name="descriptionTextAppearance" format="reference" /> + <attr name="passwordTextAppearance" format="reference" /> + <attr name="errorTextAppearance" format="reference"/> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 637ac1911a85..d4d8843acdea 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -641,7 +641,7 @@ <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] --> <string name="quick_settings_color_correction_label">Color correction</string> <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] --> - <string name="quick_settings_more_user_settings">User settings</string> + <string name="quick_settings_more_user_settings">Manage users</string> <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] --> <string name="quick_settings_done">Done</string> <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 475ca919c3bf..e76887babc50 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -195,15 +195,11 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> - <style name="TextAppearance.AuthNonBioCredential.Icon"> - <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> - <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> - </style> - <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">google-sans</item> - <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">36sp</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> </style> <style name="TextAppearance.AuthNonBioCredential.Subtitle"> @@ -215,12 +211,10 @@ <style name="TextAppearance.AuthNonBioCredential.Description"> <item name="android:fontFamily">google-sans</item> <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">16sp</item> + <item name="android:textSize">18sp</item> </style> <style name="TextAppearance.AuthNonBioCredential.Error"> - <item name="android:paddingTop">6dp</item> - <item name="android:paddingBottom">18dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:attr/colorError</item> @@ -239,12 +233,33 @@ <style name="AuthCredentialHeaderStyle"> <item name="android:paddingStart">48dp</item> <item name="android:paddingEnd">48dp</item> - <item name="android:paddingTop">28dp</item> - <item name="android:paddingBottom">20dp</item> - <item name="android:orientation">vertical</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> <item name="android:layout_gravity">top</item> </style> + <style name="AuthCredentialIconStyle"> + <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> + <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:padding">20dp</item> + </style> + + <style name="AuthCredentialPinPasswordContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">48dp</item> + <item name="android:maxWidth">600dp</item> + <item name="android:minHeight">48dp</item> + <item name="android:minWidth">200dp</item> + </style> + <style name="DeviceManagementDialogTitle"> <item name="android:gravity">center</item> <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item> @@ -282,7 +297,9 @@ <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item> <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item> <item name="android:colorError">@*android:color/error_color_material_dark</item> - <item name="*android:lockPatternStyle">@style/LockPatternStyle</item> + <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item> + <item name="lockPatternStyle">@style/LockPatternContainerStyle</item> + <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item> <item name="passwordStyle">@style/PasswordTheme</item> <item name="numPadKeyStyle">@style/NumPadKey</item> <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item> @@ -308,27 +325,33 @@ <item name="android:textColor">?attr/wallpaperTextColor</item> </style> - <style name="LockPatternContainerStyle"> - <item name="android:maxHeight">400dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">0dp</item> - <item name="android:minWidth">0dp</item> - <item name="android:paddingHorizontal">60dp</item> - <item name="android:paddingBottom">40dp</item> + <style name="AuthCredentialStyle"> + <item name="*android:regularColor">?android:attr/colorForeground</item> + <item name="*android:successColor">?android:attr/colorForeground</item> + <item name="*android:errorColor">?android:attr/colorError</item> + <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <item name="headerStyle">@style/AuthCredentialHeaderStyle</item> + <item name="headerIconStyle">@style/AuthCredentialIconStyle</item> + <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item> + <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item> + <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item> + <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item> + <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item> </style> - <style name="LockPatternStyle"> + <style name="LockPatternViewStyle" > <item name="*android:regularColor">?android:attr/colorAccent</item> <item name="*android:successColor">?android:attr/textColorPrimary</item> <item name="*android:errorColor">?android:attr/colorError</item> <item name="*android:dotColor">?android:attr/textColorSecondary</item> </style> - <style name="LockPatternStyleBiometricPrompt"> - <item name="*android:regularColor">?android:attr/colorForeground</item> - <item name="*android:successColor">?android:attr/colorForeground</item> - <item name="*android:errorColor">?android:attr/colorError</item> - <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item> + </style> + + <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item> </style> <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault"> diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt index 2e391c7aacbe..49cc48321d77 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt @@ -19,6 +19,7 @@ package com.android.systemui.testing.screenshot import android.app.Activity import android.graphics.Color import android.view.View +import android.view.Window import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -51,13 +52,14 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR /** * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in - * the context of [emulationSpec]. + * the context of [emulationSpec]. Window must be specified to capture views that render + * hardware buffers. */ - fun screenshotTest(goldenIdentifier: String, view: View) { + fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) { view.removeElevationRecursively() ScreenshotRuleAsserter.Builder(screenshotRule) - .setScreenshotProvider { view.toBitmap() } + .setScreenshotProvider { view.toBitmap(window) } .withMatcher(matcher) .build() .assertGoldenImage(goldenIdentifier) @@ -94,6 +96,6 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR activity.currentFocus?.clearFocus() } - screenshotTest(goldenIdentifier, rootView) + screenshotTest(goldenIdentifier, rootView, activity.window) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 134f3bc93847..1cf7c503a508 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -190,8 +190,13 @@ class AnimatableClockView @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { lastDraw = getTimestamp() - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) + // Use textAnimator to render text if animation is enabled. + // Otherwise default to using standard draw functions. + if (isAnimationEnabled) { + textAnimator?.draw(canvas) + } else { + super.onDraw(canvas) + } } override fun invalidate() { @@ -363,6 +368,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -377,6 +385,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index e3c21cca2263..cd272635905b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -21,7 +21,6 @@ import android.os.Handler import android.os.UserHandle import android.provider.Settings import android.util.Log -import com.android.systemui.dagger.qualifiers.Main import com.android.internal.annotations.Keep import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId @@ -31,7 +30,6 @@ import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.PluginListener import com.android.systemui.shared.plugins.PluginManager import com.google.gson.Gson -import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName private const val DEBUG = true @@ -43,13 +41,6 @@ open class ClockRegistry( val handler: Handler, defaultClockProvider: ClockProvider ) { - @Inject constructor( - context: Context, - pluginManager: PluginManager, - @Main handler: Handler, - defaultClockProvider: DefaultClockProvider - ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } - // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { fun onClockChanged() diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index 72f8b7b09dca..40c8774d4f34 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -1,13 +1,16 @@ package com.android.systemui.shared.recents.utilities; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Surface; import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.wm.shell.util.SplitBounds; /** * Utility class to position the thumbnail in the TaskView @@ -16,10 +19,26 @@ public class PreviewPositionHelper { public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f; + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. private final RectF mClippedInsets = new RectF(); private final Matrix mMatrix = new Matrix(); private boolean mIsOrientationChanged; + private SplitBounds mSplitBounds; + private int mDesiredStagePosition; public Matrix getMatrix() { return mMatrix; @@ -33,6 +52,11 @@ public class PreviewPositionHelper { return mIsOrientationChanged; } + public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) { + mSplitBounds = splitBounds; + mDesiredStagePosition = desiredStagePosition; + } + /** * Updates the matrix based on the provided parameters */ @@ -42,10 +66,19 @@ public class PreviewPositionHelper { boolean isRotated = false; boolean isOrientationDifferent; + float fullscreenTaskWidth = screenWidthPx; + if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + } int thumbnailRotation = thumbnailData.rotation; int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float canvasScreenRatio = canvasWidth / (float) screenWidthPx; + float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; float scaledTaskbarSize = taskbarSize * canvasScreenRatio; thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; @@ -180,7 +213,7 @@ public class PreviewPositionHelper { * portrait or vice versa, {@code false} otherwise */ private boolean isOrientationChange(int deltaRotation) { - return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; + return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270; } private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) { @@ -189,13 +222,13 @@ public class PreviewPositionHelper { mMatrix.setRotate(90 * deltaRotate); switch (deltaRotate) { /* Counter-clockwise */ - case Surface.ROTATION_90: + case ROTATION_90: translateX = thumbnailPosition.height(); break; - case Surface.ROTATION_270: + case ROTATION_270: translateY = thumbnailPosition.width(); break; - case Surface.ROTATION_180: + case ROTATION_180: translateX = thumbnailPosition.width(); translateY = thumbnailPosition.height(); break; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index 5d6598d63a1b..8a2509610310 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -51,6 +51,8 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; + public static final int CUJ_RECENTS_SCROLLING = + InteractionJankMonitor.CUJ_RECENTS_SCROLLING; @IntDef({ CUJ_APP_LAUNCH_FROM_RECENTS, @@ -59,7 +61,8 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_CLOSE_TO_PIP, CUJ_QUICK_SWITCH, CUJ_APP_LAUNCH_FROM_WIDGET, - CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION + CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index 5277e40492e4..450784ea8f03 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -70,7 +70,7 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : } override fun setMessage(msg: CharSequence?) { - if (msg == textAboutToShow || msg == text) { + if ((msg == textAboutToShow && msg != null) || msg == text) { return } textAboutToShow = msg diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 9151238499dc..910955a45f7b 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -23,12 +23,18 @@ import android.content.res.Resources import android.text.format.DateFormat import android.util.TypedValue import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.broadcast.BroadcastDispatcher 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.REGION_SAMPLING +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ClockController -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -38,13 +44,20 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. */ open class ClockEventController @Inject constructor( - private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -53,7 +66,7 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { @@ -70,9 +83,9 @@ open class ClockEventController @Inject constructor( private var isCharging = false private var dozeAmount = 0f private var isKeyguardVisible = false - - private val regionSamplingEnabled = - featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) + private var isRegistered = false + private var disposableHandle: DisposableHandle? = null + private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) private fun updateColors() { if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { @@ -165,15 +178,6 @@ open class ClockEventController @Inject constructor( } } - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - clock?.animations?.doze(linear) - - isDozing = linear > dozeAmount - dozeAmount = linear - } - } - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible @@ -195,13 +199,11 @@ open class ClockEventController @Inject constructor( } } - init { - isDozing = statusBarStateController.isDozing - } - - fun registerListeners() { - dozeAmount = statusBarStateController.dozeAmount - isDozing = statusBarStateController.isDozing || dozeAmount != 0f + fun registerListeners(parent: View) { + if (isRegistered) { + return + } + isRegistered = true broadcastDispatcher.registerReceiver( localeBroadcastReceiver, @@ -210,17 +212,28 @@ open class ClockEventController @Inject constructor( configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - statusBarStateController.addCallback(statusBarStateListener) smallRegionSampler?.startRegionSampler() largeRegionSampler?.startRegionSampler() + disposableHandle = parent.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + listenForDozing(this) + listenForDozeAmount(this) + listenForDozeAmountTransition(this) + } + } } fun unregisterListeners() { + if (!isRegistered) { + return + } + isRegistered = false + + disposableHandle?.dispose() broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) - statusBarStateController.removeCallback(statusBarStateListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() } @@ -235,8 +248,39 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.dump(pw) } - companion object { - private val TAG = ClockEventController::class.simpleName - private const val FORMAT_NUMBER = 1234567890 + @VisibleForTesting + internal fun listenForDozeAmount(scope: CoroutineScope): Job { + return scope.launch { + keyguardInteractor.dozeAmount.collect { + dozeAmount = it + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.aodToLockscreenTransition.collect { + // Would eventually run this: + // dozeAmount = it.value + // clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { + return scope.launch { + combine ( + keyguardInteractor.dozeAmount, + keyguardInteractor.isDozing, + ) { localDozeAmount, localIsDozing -> + localDozeAmount > dozeAmount || localIsDozing + } + .collect { localIsDozing -> + isDozing = localIsDozing + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 20d064b960d2..8eebe30222ae 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -165,7 +165,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onViewAttached() { mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); - mClockEventController.registerListeners(); + mClockEventController.registerListeners(mView); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index bcd1a1ee2696..81305f90e2b8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -219,13 +219,16 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard }; - private SwipeListener mSwipeListener = new SwipeListener() { + private final SwipeListener mSwipeListener = new SwipeListener() { @Override public void onSwipeUp() { if (!mUpdateMonitor.isFaceDetectionRunning()) { - mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); mKeyguardSecurityCallback.userActivity(); - showMessage(null, null); + if (didFaceAuthRun) { + showMessage(null, null); + } } if (mUpdateMonitor.isFaceEnrolled()) { mUpdateMonitor.requestActiveUnlock( @@ -234,7 +237,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } }; - private ConfigurationController.ConfigurationListener mConfigurationListener = + private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @Override public void onThemeChanged() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index f5582761c0ae..aff9dcbc26e3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -106,7 +106,6 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; @@ -2352,11 +2351,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @param userInitiatedRequest true if the user explicitly requested face auth * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being * invoked. + * @return current face auth detection state, true if it is running. */ - public void requestFaceAuth(boolean userInitiatedRequest, + public boolean requestFaceAuth(boolean userInitiatedRequest, @FaceAuthApiRequestReason String reason) { mLogger.logFaceAuthRequested(userInitiatedRequest, reason); updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason)); + return isFaceDetectionRunning(); } /** @@ -2366,10 +2367,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER); } - public boolean isFaceScanning() { - return mFaceRunningState == BIOMETRIC_STATE_RUNNING; - } - private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) { // If this message exists, we should not authenticate again until this message is // consumed by the handler @@ -2417,7 +2414,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Attempts to trigger active unlock from trust agent. */ private void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String reason, boolean dismissKeyguard ) { @@ -2447,7 +2444,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Only dismisses the keyguard under certain conditions. */ public void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String extraReason ) { final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null @@ -2714,7 +2711,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return shouldListen; } - private void maybeLogListenerModelData(KeyguardListenModel model) { + private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardActiveUnlockModel) { @@ -3796,4 +3793,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } mListenModels.print(pw); } + + /** + * Schedules a watchdog for the face and fingerprint BiometricScheduler. + * Cancels all operations in the scheduler if it is hung for 10 seconds. + */ + public void startBiometricWatchdog() { + if (mFaceManager != null) { + mFaceManager.scheduleWatchdog(); + } + if (mFpm != null) { + mFpm.scheduleWatchdog(); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java index c4be1ba53503..72a44bd198f2 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java @@ -21,9 +21,14 @@ import java.util.List; import dagger.Module; import dagger.Provides; -/** Dagger Module for clock package. */ +/** + * Dagger Module for clock package. + * + * @deprecated Migrate to ClockRegistry + */ @Module -public abstract class ClockModule { +@Deprecated +public abstract class ClockInfoModule { /** */ @Provides diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java new file mode 100644 index 000000000000..f43f559b4234 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard.dagger; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.shared.clocks.ClockRegistry; +import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.plugins.PluginManager; + +import dagger.Module; +import dagger.Provides; + +/** Dagger Module for clocks. */ +@Module +public abstract class ClockRegistryModule { + /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */ + @Provides + @SysUISingleton + public static ClockRegistry getClockRegistry( + @Application Context context, + PluginManager pluginManager, + @Main Handler handler, + DefaultClockProvider defaultClockProvider) { + return new ClockRegistry(context, pluginManager, handler, defaultClockProvider); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 82b32cf616ec..2f79e30a0b5b 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -51,7 +51,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg) - fun logActiveUnlockTriggered(reason: String) { + fun logActiveUnlockTriggered(reason: String?) { logBuffer.log("ActiveUnlock", DEBUG, { str1 = reason }, { "initiate active unlock triggerReason=$str1" }) @@ -101,14 +101,14 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "Face authenticated for wrong user: $int1" }) } - fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) { + fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) { logBuffer.log(TAG, DEBUG, { int1 = msgId str1 = helpMsg }, { "Face help received, msgId: $int1 msg: $str1" }) } - fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String) { + fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) { logBuffer.log(TAG, DEBUG, { bool1 = userInitiatedRequest str1 = reason @@ -187,7 +187,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "No Profile Owner or Device Owner supervision app found for User $int1" }) } - fun logPhoneStateChanged(newState: String) { + fun logPhoneStateChanged(newState: String?) { logBuffer.log(TAG, DEBUG, { str1 = newState }, { "handlePhoneStateChanged($str1)" }) @@ -240,7 +240,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }) } - fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) { + fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = "$serviceState" @@ -256,7 +256,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }) } - fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) { + fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = extraSimState @@ -289,7 +289,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "SubInfo:$str1" }) } - fun logTimeFormatChanged(newTimeFormat: String) { + fun logTimeFormatChanged(newTimeFormat: String?) { logBuffer.log(TAG, DEBUG, { str1 = newTimeFormat }, { "handleTimeFormatUpdate timeFormat=$str1" }) @@ -338,18 +338,18 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun logUserRequestedUnlock( requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN, - reason: String, + reason: String?, dismissKeyguard: Boolean ) { logBuffer.log("ActiveUnlock", DEBUG, { - str1 = requestOrigin.name + str1 = requestOrigin?.name str2 = reason bool1 = dismissKeyguard }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }) } fun logShowTrustGrantedMessage( - message: String + message: String? ) { logBuffer.log(TAG, DEBUG, { str1 = message diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java index 652595100c0f..73fdce6c9045 100644 --- a/packages/SystemUI/src/com/android/systemui/Dumpable.java +++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java @@ -30,7 +30,6 @@ public interface Dumpable { /** * Called when it's time to dump the internal state - * @param fd A file descriptor. * @param pw Where to write your dump to. * @param args Arguments. */ diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index c5955860aebf..3e0fa455d39e 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet +import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas @@ -55,7 +56,7 @@ class FaceScanningOverlay( private val rimRect = RectF() private var cameraProtectionColor = Color.BLACK var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context, - com.android.systemui.R.attr.wallpaperTextColorAccent) + R.attr.wallpaperTextColorAccent) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null var faceAuthSucceeded = false @@ -84,46 +85,19 @@ class FaceScanningOverlay( } override fun drawCutoutProtection(canvas: Canvas) { - if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) { - val rimPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val rimBounds = RectF() - rimPath.computeBounds(rimBounds, true) - setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY()) - } - rimPath.transform(scaleMatrix) - rimPaint.style = Paint.Style.FILL - val rimPaintAlpha = rimPaint.alpha - rimPaint.color = ColorUtils.blendARGB( - faceScanningAnimColor, - Color.WHITE, - statusBarStateController.dozeAmount) - rimPaint.alpha = rimPaintAlpha - canvas.drawPath(rimPath, rimPaint) + if (protectionRect.isEmpty) { + return } - - if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && - !protectionRect.isEmpty) { - val scaledProtectionPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val protectionPathRect = RectF() - scaledProtectionPath.computeBounds(protectionPathRect, true) - setScale(cameraProtectionProgress, cameraProtectionProgress, - protectionPathRect.centerX(), protectionPathRect.centerY()) - } - scaledProtectionPath.transform(scaleMatrix) - paint.style = Paint.Style.FILL - paint.color = cameraProtectionColor - canvas.drawPath(scaledProtectionPath, paint) + if (rimProgress > HIDDEN_RIM_SCALE) { + drawFaceScanningRim(canvas) + } + if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) { + drawCameraProtection(canvas) } - } - - override fun updateVisOnUpdateCutout(): Boolean { - return false // instead, we always update the visibility whenever face scanning starts/ends } override fun enableShowProtection(show: Boolean) { - val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show + val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show if (showScanningAnimNow == showScanningAnim) { return } @@ -152,91 +126,26 @@ class FaceScanningOverlay( if (showScanningAnim) Interpolators.STANDARD_ACCELERATE else if (faceAuthSucceeded) Interpolators.STANDARD else Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - cameraProtectionProgress = animation.animatedValue as Float - invalidate() - }) + addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress) addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { cameraProtectionAnimator = null if (!showScanningAnim) { - visibility = View.INVISIBLE - hideOverlayRunnable?.run() - hideOverlayRunnable = null - requestLayout() + hide() } } }) } rimAnimator?.cancel() - rimAnimator = AnimatorSet().apply { - if (showScanningAnim) { - val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE, - PULSE_RADIUS_OUT).apply { - duration = PULSE_APPEAR_DURATION - interpolator = Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - - // animate in camera protection, rim, and then pulse in/out - playSequentially(cameraProtectionAnimator, rimAppearAnimator, - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator()) - } else { - val rimDisappearAnimator = ValueAnimator.ofFloat( - rimProgress, - if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS - else SHOW_CAMERA_PROTECTION_SCALE - ).apply { - duration = - if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION - else PULSE_ERROR_DISAPPEAR_DURATION - interpolator = - if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE - else Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimProgress = HIDDEN_RIM_SCALE - invalidate() - } - }) - } - if (faceAuthSucceeded) { - val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply { - duration = PULSE_SUCCESS_DISAPPEAR_DURATION - interpolator = Interpolators.LINEAR - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimPaint.alpha = animation.animatedValue as Int - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimPaint.alpha = 255 - invalidate() - } - }) - } - val rimSuccessAnimator = AnimatorSet() - rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator) - playTogether(rimSuccessAnimator, cameraProtectionAnimator) - } else { - playTogether(rimDisappearAnimator, cameraProtectionAnimator) - } - } - + rimAnimator = if (showScanningAnim) { + createFaceScanningRimAnimator() + } else if (faceAuthSucceeded) { + createFaceSuccessRimAnimator() + } else { + createFaceNotSuccessRimAnimator() + } + rimAnimator?.apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { rimAnimator = null @@ -245,34 +154,12 @@ class FaceScanningOverlay( } } }) - start() } + rimAnimator?.start() } - fun createPulseAnimator(): AnimatorSet { - return AnimatorSet().apply { - val pulseInwards = ValueAnimator.ofFloat( - PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { - duration = PULSE_DURATION_INWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - val pulseOutwards = ValueAnimator.ofFloat( - PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply { - duration = PULSE_DURATION_OUTWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - playSequentially(pulseInwards, pulseOutwards) - } + override fun updateVisOnUpdateCutout(): Boolean { + return false // instead, we always update the visibility whenever face scanning starts/ends } override fun updateProtectionBoundingPath() { @@ -290,17 +177,153 @@ class FaceScanningOverlay( // Make sure that our measured height encompasses the extra space for the animation mTotalBounds.union(mBoundingRect) mTotalBounds.union( - rimRect.left.toInt(), - rimRect.top.toInt(), - rimRect.right.toInt(), - rimRect.bottom.toInt()) + rimRect.left.toInt(), + rimRect.top.toInt(), + rimRect.right.toInt(), + rimRect.bottom.toInt()) setMeasuredDimension( - resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), - resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), + resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) } else { setMeasuredDimension( - resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), - resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), + resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + } + } + + private fun drawFaceScanningRim(canvas: Canvas) { + val rimPath = Path(protectionPath) + scalePath(rimPath, rimProgress) + rimPaint.style = Paint.Style.FILL + val rimPaintAlpha = rimPaint.alpha + rimPaint.color = ColorUtils.blendARGB( + faceScanningAnimColor, + Color.WHITE, + statusBarStateController.dozeAmount + ) + rimPaint.alpha = rimPaintAlpha + canvas.drawPath(rimPath, rimPaint) + } + + private fun drawCameraProtection(canvas: Canvas) { + val scaledProtectionPath = Path(protectionPath) + scalePath(scaledProtectionPath, cameraProtectionProgress) + paint.style = Paint.Style.FILL + paint.color = cameraProtectionColor + canvas.drawPath(scaledProtectionPath, paint) + } + + private fun createFaceSuccessRimAnimator(): AnimatorSet { + val rimSuccessAnimator = AnimatorSet() + rimSuccessAnimator.playTogether( + createRimDisappearAnimator( + PULSE_RADIUS_SUCCESS, + PULSE_SUCCESS_DISAPPEAR_DURATION, + Interpolators.STANDARD_DECELERATE + ), + createSuccessOpacityAnimator(), + ) + return AnimatorSet().apply { + playTogether(rimSuccessAnimator, cameraProtectionAnimator) + } + } + + private fun createFaceNotSuccessRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playTogether( + createRimDisappearAnimator( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_ERROR_DISAPPEAR_DURATION, + Interpolators.STANDARD + ), + cameraProtectionAnimator, + ) + } + } + + private fun createRimDisappearAnimator( + endValue: Float, + animDuration: Long, + timeInterpolator: TimeInterpolator + ): ValueAnimator { + return ValueAnimator.ofFloat(rimProgress, endValue).apply { + duration = animDuration + interpolator = timeInterpolator + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + invalidate() + } + }) + } + } + + private fun createSuccessOpacityAnimator(): ValueAnimator { + return ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_SUCCESS_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(this@FaceScanningOverlay::updateRimAlpha) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimPaint.alpha = 255 + invalidate() + } + }) + } + } + + private fun createFaceScanningRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playSequentially( + cameraProtectionAnimator, + createRimAppearAnimator(), + createPulseAnimator() + ) + } + } + + private fun createRimAppearAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_RADIUS_OUT + ).apply { + duration = PULSE_APPEAR_DURATION + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + } + } + + private fun hide() { + visibility = INVISIBLE + hideOverlayRunnable?.run() + hideOverlayRunnable = null + requestLayout() + } + + private fun updateRimProgress(animator: ValueAnimator) { + rimProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateCameraProtectionProgress(animator: ValueAnimator) { + cameraProtectionProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateRimAlpha(animator: ValueAnimator) { + rimPaint.alpha = animator.animatedValue as Int + invalidate() + } + + private fun createPulseAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { + duration = HALF_PULSE_DURATION + interpolator = Interpolators.STANDARD + repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times + repeatMode = ValueAnimator.REVERSE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) } } @@ -363,13 +386,24 @@ class FaceScanningOverlay( private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L private const val PULSE_APPEAR_DURATION = 250L // without start delay - private const val PULSE_DURATION_INWARDS = 500L - private const val PULSE_DURATION_OUTWARDS = 500L + private const val HALF_PULSE_DURATION = 500L private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay + + private fun scalePath(path: Path, scalingFactor: Float) { + val scaleMatrix = Matrix().apply { + val boundingRectangle = RectF() + path.computeBounds(boundingRectangle, true) + setScale( + scalingFactor, scalingFactor, + boundingRectangle.centerX(), boundingRectangle.centerY() + ) + } + path.transform(scaleMatrix) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt new file mode 100644 index 000000000000..4c3a7ff4e2eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt @@ -0,0 +1,7 @@ +package com.android.systemui + +import com.android.systemui.dump.nano.SystemUIProtoDump + +interface ProtoDumpable : Dumpable { + fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 7bcba3cc1c46..50e03992df49 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -121,6 +121,6 @@ public class SystemUIService extends Service { DumpHandler.PRIORITY_ARG_CRITICAL}; } - mDumpHandler.dump(pw, massagedArgs); + mDumpHandler.dump(fd, pw, massagedArgs); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java new file mode 100644 index 000000000000..d6d039903505 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static android.util.MathUtils.constrain; + +import static java.util.Objects.requireNonNull; + +import android.animation.ValueAnimator; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FlingAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.HashMap; + +/** + * Controls the interaction animations of the {@link MenuView}. Also, it will use the relative + * coordinate based on the {@link MenuViewLayer} to compute the offset of the {@link MenuView}. + */ +class MenuAnimationController { + private static final String TAG = "MenuAnimationController"; + private static final boolean DEBUG = false; + private static final float MIN_PERCENT = 0.0f; + private static final float MAX_PERCENT = 1.0f; + private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float FLING_FRICTION_SCALAR = 1.9f; + private static final float DEFAULT_FRICTION = 4.2f; + private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; + private static final float SPRING_STIFFNESS = 700f; + private static final float ESCAPE_VELOCITY = 750f; + + private static final int FADE_OUT_DURATION_MS = 1000; + private static final int FADE_EFFECT_DURATION_MS = 3000; + + private final MenuView mMenuView; + private final ValueAnimator mFadeOutAnimator; + private final Handler mHandler; + private boolean mIsMovedToEdge; + private boolean mIsFadeEffectEnabled; + + // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link + // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler + private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations = + new HashMap<>(); + + MenuAnimationController(MenuView menuView) { + mMenuView = menuView; + + mHandler = createUiHandler(); + mFadeOutAnimator = new ValueAnimator(); + mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS); + mFadeOutAnimator.addUpdateListener( + (animation) -> menuView.setAlpha((float) animation.getAnimatedValue())); + } + + void moveToPosition(PointF position) { + moveToPositionX(position.x); + moveToPositionY(position.y); + } + + void moveToPositionX(float positionX) { + DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX); + } + + private void moveToPositionY(float positionY) { + DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY); + } + + void moveToPositionYIfNeeded(float positionY) { + // If the list view was out of screen bounds, it would allow users to nest scroll inside + // and avoid conflicting with outer scroll. + final RecyclerView listView = (RecyclerView) mMenuView.getChildAt(/* index= */ 0); + if (listView.getOverScrollMode() == View.OVER_SCROLL_NEVER) { + moveToPositionY(positionY); + } + } + + void moveToTopLeftPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top)); + } + + void moveToTopRightPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top)); + } + + void moveToBottomLeftPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom)); + } + + void moveToBottomRightPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom)); + } + + void moveAndPersistPosition(PointF position) { + moveToPosition(position); + mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); + constrainPositionAndUpdate(position); + } + + void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) { + final boolean shouldMenuFlingLeft = isOnLeftSide() + ? velocityX < ESCAPE_VELOCITY + : velocityX < -ESCAPE_VELOCITY; + + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + final float finalPositionX = shouldMenuFlingLeft + ? draggableBounds.left : draggableBounds.right; + + final float minimumVelocityToReachEdge = + (finalPositionX - x) * (FLING_FRICTION_SCALAR * DEFAULT_FRICTION); + + final float startXVelocity = shouldMenuFlingLeft + ? Math.min(minimumVelocityToReachEdge, velocityX) + : Math.max(minimumVelocityToReachEdge, velocityX); + + flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X, + startXVelocity, + FLING_FRICTION_SCALAR, + new SpringForce() + .setStiffness(SPRING_STIFFNESS) + .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), + finalPositionX); + + flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y, + velocityY, + FLING_FRICTION_SCALAR, + new SpringForce() + .setStiffness(SPRING_STIFFNESS) + .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), + /* finalPosition= */ null); + } + + private void flingThenSpringMenuWith(DynamicAnimation.ViewProperty property, float velocity, + float friction, SpringForce spring, Float finalPosition) { + + final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property); + final float currentValue = menuPositionProperty.getValue(mMenuView); + final Rect bounds = mMenuView.getMenuDraggableBounds(); + final float min = + property.equals(DynamicAnimation.TRANSLATION_X) + ? bounds.left + : bounds.top; + final float max = + property.equals(DynamicAnimation.TRANSLATION_X) + ? bounds.right + : bounds.bottom; + + final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty); + flingAnimation.setFriction(friction) + .setStartVelocity(velocity) + .setMinValue(Math.min(currentValue, min)) + .setMaxValue(Math.max(currentValue, max)) + .addEndListener((animation, canceled, endValue, endVelocity) -> { + if (canceled) { + if (DEBUG) { + Log.d(TAG, "The fling animation was canceled."); + } + + return; + } + + final float endPosition = finalPosition != null + ? finalPosition + : Math.max(min, Math.min(max, endValue)); + springMenuWith(property, spring, endVelocity, endPosition); + }); + + cancelAnimation(property); + mPositionAnimations.put(property, flingAnimation); + flingAnimation.start(); + } + + private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring, + float velocity, float finalPosition) { + final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property); + final SpringAnimation springAnimation = + new SpringAnimation(mMenuView, menuPositionProperty) + .setSpring(spring) + .addEndListener((animation, canceled, endValue, endVelocity) -> { + if (canceled || endValue != finalPosition) { + return; + } + + onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(), + mMenuView.getTranslationY())); + }) + .setStartVelocity(velocity); + + cancelAnimation(property); + mPositionAnimations.put(property, springAnimation); + springAnimation.animateToFinalPosition(finalPosition); + } + + /** + * Determines whether to hide the menu to the edge of the screen with the given current + * translation x of the menu view. It should be used when receiving the action up touch event. + * + * @param currentXTranslation the current translation x of the menu view. + * @return true if the menu would be hidden to the edge, otherwise false. + */ + boolean maybeMoveToEdgeAndHide(float currentXTranslation) { + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + + // If the translation x is zero, it should be at the left of the bound. + if (currentXTranslation < draggableBounds.left + || currentXTranslation > draggableBounds.right) { + moveToEdgeAndHide(); + return true; + } + + fadeOutIfEnabled(); + return false; + } + + private boolean isOnLeftSide() { + return mMenuView.getTranslationX() < mMenuView.getMenuDraggableBounds().centerX(); + } + + boolean isMovedToEdge() { + return mIsMovedToEdge; + } + + void moveToEdgeAndHide() { + mIsMovedToEdge = true; + + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + final float endY = constrain(mMenuView.getTranslationY(), draggableBounds.top, + draggableBounds.bottom); + final float menuHalfWidth = mMenuView.getWidth() / 2.0f; + final float endX = isOnLeftSide() + ? draggableBounds.left - menuHalfWidth + : draggableBounds.right + menuHalfWidth; + moveAndPersistPosition(new PointF(endX, endY)); + + // Keep the touch region let users could click extra space to pop up the menu view + // from the screen edge + mMenuView.onBoundsInParentChanged(isOnLeftSide() + ? draggableBounds.left + : draggableBounds.right, (int) mMenuView.getTranslationY()); + + fadeOutIfEnabled(); + } + + void moveOutEdgeAndShow() { + mIsMovedToEdge = false; + + mMenuView.onPositionChanged(); + mMenuView.onEdgeChangedIfNeeded(); + } + + void cancelAnimations() { + cancelAnimation(DynamicAnimation.TRANSLATION_X); + cancelAnimation(DynamicAnimation.TRANSLATION_Y); + } + + private void cancelAnimation(DynamicAnimation.ViewProperty property) { + if (!mPositionAnimations.containsKey(property)) { + return; + } + + mPositionAnimations.get(property).cancel(); + } + + void onDraggingStart() { + mMenuView.onDraggingStart(); + } + + private void onSpringAnimationEnd(PointF position) { + mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); + constrainPositionAndUpdate(position); + + fadeOutIfEnabled(); + } + + private void constrainPositionAndUpdate(PointF position) { + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + // Have the space gap margin between the top bound and the menu view, so actually the + // position y range needs to cut the margin. + position.offset(-draggableBounds.left, -draggableBounds.top); + + final float percentageX = position.x < draggableBounds.centerX() + ? MIN_PERCENT : MAX_PERCENT; + + final float percentageY = position.y < 0 || draggableBounds.height() == 0 + ? MIN_PERCENT + : Math.min(MAX_PERCENT, position.y / draggableBounds.height()); + mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY)); + } + + void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) { + mIsFadeEffectEnabled = isFadeEffectEnabled; + + mHandler.removeCallbacksAndMessages(/* token= */ null); + mFadeOutAnimator.cancel(); + mFadeOutAnimator.setFloatValues(COMPLETELY_OPAQUE, newOpacityValue); + mHandler.post(() -> mMenuView.setAlpha( + mIsFadeEffectEnabled ? newOpacityValue : COMPLETELY_OPAQUE)); + } + + void fadeInNowIfEnabled() { + if (!mIsFadeEffectEnabled) { + return; + } + + cancelAndRemoveCallbacksAndMessages(); + mHandler.post(() -> mMenuView.setAlpha(COMPLETELY_OPAQUE)); + } + + void fadeOutIfEnabled() { + if (!mIsFadeEffectEnabled) { + return; + } + + cancelAndRemoveCallbacksAndMessages(); + mHandler.postDelayed(mFadeOutAnimator::start, FADE_EFFECT_DURATION_MS); + } + + private void cancelAndRemoveCallbacksAndMessages() { + mFadeOutAnimator.cancel(); + mHandler.removeCallbacksAndMessages(/* token= */ null); + } + + private Handler createUiHandler() { + return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null")); + } + + static class MenuPositionProperty + extends FloatPropertyCompat<MenuView> { + private final DynamicAnimation.ViewProperty mProperty; + + MenuPositionProperty(DynamicAnimation.ViewProperty property) { + super(property.toString()); + mProperty = property; + } + + @Override + public float getValue(MenuView menuView) { + return mProperty.getValue(menuView); + } + + @Override + public void setValue(MenuView menuView, float value) { + mProperty.setValue(menuView, value); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt new file mode 100644 index 000000000000..83c344cca214 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu + +import android.annotation.FloatRange + +@FloatRange(from = 0.0, to = 1.0) const val DEFAULT_OPACITY_VALUE = 0.55f +const val DEFAULT_FADE_EFFECT_IS_ENABLED = 1 + +/** The data class for the fade effect info of the accessibility floating menu view. */ +data class MenuFadeEffectInfo(val isFadeEffectEnabled: Boolean, val opacity: Float) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 698d60a5b13e..57019de762a4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -16,22 +16,29 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED; +import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY; import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE; import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; +import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED; +import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_OPACITY_VALUE; import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL; +import android.annotation.FloatRange; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Prefs; import java.util.List; @@ -39,9 +46,16 @@ import java.util.List; * Stores and observe the settings contents for the menu view. */ class MenuInfoRepository { + @FloatRange(from = 0.0, to = 1.0) + private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f; + + @FloatRange(from = 0.0, to = 1.0) + private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.9f; + private final Context mContext; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final OnSettingsContentsChanged mSettingsContentsCallback; + private Position mPercentagePosition; private final ContentObserver mMenuTargetFeaturesContentObserver = new ContentObserver(mHandler) { @@ -62,9 +76,24 @@ class MenuInfoRepository { } }; + @VisibleForTesting + final ContentObserver mMenuFadeOutContentObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + mSettingsContentsCallback.onFadeEffectInfoChanged(getMenuFadeEffectInfo()); + } + }; + MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) { mContext = context; mSettingsContentsCallback = settingsContentsChanged; + + mPercentagePosition = getStartPosition(); + } + + void loadMenuPosition(OnInfoReady<Position> callback) { + callback.onReady(mPercentagePosition); } void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) { @@ -75,6 +104,30 @@ class MenuInfoRepository { callback.onReady(getMenuSizeTypeFromSettings(mContext)); } + void loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback) { + callback.onReady(getMenuFadeEffectInfo()); + } + + private MenuFadeEffectInfo getMenuFadeEffectInfo() { + return new MenuFadeEffectInfo(isMenuFadeEffectEnabledFromSettings(mContext), + getMenuOpacityFromSettings(mContext)); + } + + void updateMenuSavingPosition(Position percentagePosition) { + mPercentagePosition = percentagePosition; + Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, + percentagePosition.toString()); + } + + private Position getStartPosition() { + final String absolutePositionString = Prefs.getString(mContext, + Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); + + return TextUtils.isEmpty(absolutePositionString) + ? new Position(DEFAULT_MENU_POSITION_X_PERCENT, DEFAULT_MENU_POSITION_Y_PERCENT) + : Position.fromString(absolutePositionString); + } + void registerContentObservers() { mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), @@ -88,17 +141,28 @@ class MenuInfoRepository { Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE), /* notifyForDescendants */ false, mMenuSizeContentObserver, UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED), + /* notifyForDescendants */ false, mMenuFadeOutContentObserver, + UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY), + /* notifyForDescendants */ false, mMenuFadeOutContentObserver, + UserHandle.USER_CURRENT); } void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver); mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver); + mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver); } interface OnSettingsContentsChanged { void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures); void onSizeTypeChanged(int newSizeType); + + void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo); } interface OnInfoReady<T> { @@ -109,4 +173,16 @@ class MenuInfoRepository { return Settings.Secure.getIntForUser(context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SMALL, UserHandle.USER_CURRENT); } + + private static boolean isMenuFadeEffectEnabledFromSettings(Context context) { + return Settings.Secure.getIntForUser(context.getContentResolver(), + ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, + DEFAULT_FADE_EFFECT_IS_ENABLED, UserHandle.USER_CURRENT) == /* enabled */ 1; + } + + private static float getMenuOpacityFromSettings(Context context) { + return Settings.Secure.getFloatForUser(context.getContentResolver(), + ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE, + UserHandle.USER_CURRENT); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java new file mode 100644 index 000000000000..e69a24810fdc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS; +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS; + +import android.content.res.Resources; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; + +import com.android.systemui.R; + +/** + * An accessibility item delegate for the individual items of the list view in the + * {@link MenuView}. + */ +class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate { + private final MenuAnimationController mAnimationController; + + MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate, + MenuAnimationController animationController) { + super(recyclerViewDelegate); + mAnimationController = animationController; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + final Resources res = host.getResources(); + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left, + res.getString( + R.string.accessibility_floating_button_action_move_top_left)); + info.addAction(moveTopLeft); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_top_right, + res.getString( + R.string.accessibility_floating_button_action_move_top_right)); + info.addAction(moveTopRight); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_bottom_left, + res.getString( + R.string.accessibility_floating_button_action_move_bottom_left)); + info.addAction(moveBottomLeft); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_bottom_right, + res.getString( + R.string.accessibility_floating_button_action_move_bottom_right)); + info.addAction(moveBottomRight); + + final int moveEdgeId = mAnimationController.isMovedToEdge() + ? R.id.action_move_out_edge_and_show + : R.id.action_move_to_edge_and_hide; + final int moveEdgeTextResId = mAnimationController.isMovedToEdge() + ? R.string.accessibility_floating_button_action_move_out_edge_and_show + : R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half; + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId, + res.getString(moveEdgeTextResId)); + info.addAction(moveToOrOutEdge); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_ACCESSIBILITY_FOCUS) { + mAnimationController.fadeInNowIfEnabled(); + } + + if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) { + mAnimationController.fadeOutIfEnabled(); + } + + if (action == R.id.action_move_top_left) { + mAnimationController.moveToTopLeftPosition(); + return true; + } + + if (action == R.id.action_move_top_right) { + mAnimationController.moveToTopRightPosition(); + return true; + } + + if (action == R.id.action_move_bottom_left) { + mAnimationController.moveToBottomLeftPosition(); + return true; + } + + if (action == R.id.action_move_bottom_right) { + mAnimationController.moveToBottomRightPosition(); + return true; + } + + if (action == R.id.action_move_to_edge_and_hide) { + mAnimationController.moveToEdgeAndHide(); + return true; + } + + if (action == R.id.action_move_out_edge_and_show) { + mAnimationController.moveOutEdgeAndShow(); + return true; + } + + return super.performAccessibilityAction(host, action, args); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java new file mode 100644 index 000000000000..3146c9f0d2af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Controls the all touch events of the accessibility target features view{@link RecyclerView} in + * the {@link MenuView}. And then compute the gestures' velocity for fling and spring + * animations. + */ +class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { + private static final int VELOCITY_UNIT_SECONDS = 1000; + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private final MenuAnimationController mMenuAnimationController; + private final PointF mDown = new PointF(); + private final PointF mMenuTranslationDown = new PointF(); + private boolean mIsDragging = false; + private float mTouchSlop; + + MenuListViewTouchHandler(MenuAnimationController menuAnimationController) { + mMenuAnimationController = menuAnimationController; + } + + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent motionEvent) { + + final View menuView = (View) recyclerView.getParent(); + addMovement(motionEvent); + + final float dx = motionEvent.getRawX() - mDown.x; + final float dy = motionEvent.getRawY() - mDown.y; + + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + mMenuAnimationController.fadeInNowIfEnabled(); + mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop(); + mDown.set(motionEvent.getRawX(), motionEvent.getRawY()); + mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY()); + + mMenuAnimationController.cancelAnimations(); + break; + case MotionEvent.ACTION_MOVE: + if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) { + if (!mIsDragging) { + mIsDragging = true; + mMenuAnimationController.onDraggingStart(); + } + + mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); + mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mIsDragging) { + final float endX = mMenuTranslationDown.x + dx; + mIsDragging = false; + + if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); + mMenuAnimationController.flingMenuThenSpringToEdge(endX, + mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + } + + // Avoid triggering the listener of the item. + return true; + } + + break; + default: // Do nothing + } + + // not consume all the events here because keeping the scroll behavior of list view. + return false; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent motionEvent) { + // Do nothing + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean b) { + // Do nothing + } + + /** + * Adds a movement to the velocity tracker using raw screen coordinates. + */ + private void addMovement(MotionEvent motionEvent) { + final float deltaX = motionEvent.getRawX() - motionEvent.getX(); + final float deltaY = motionEvent.getRawY() - motionEvent.getY(); + motionEvent.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(motionEvent); + motionEvent.offsetLocation(-deltaX, -deltaY); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 576f23ee780e..15d139cf15da 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -21,28 +21,44 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; +import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** - * The container view displays the accessibility features. + * The menu view displays the accessibility features. */ @SuppressLint("ViewConstructor") -class MenuView extends FrameLayout { +class MenuView extends FrameLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { private static final int INDEX_MENU_ITEM = 0; private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>(); private final AccessibilityTargetAdapter mAdapter; private final MenuViewModel mMenuViewModel; + private final MenuAnimationController mMenuAnimationController; + private final Rect mBoundsInParent = new Rect(); private final RecyclerView mTargetFeaturesView; + private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = + this::updateSystemGestureExcludeRects; + private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver = + this::onMenuFadeEffectInfoChanged; + private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition; private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged; private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver = this::onTargetFeaturesChanged; @@ -53,23 +69,47 @@ class MenuView extends FrameLayout { mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; + mMenuAnimationController = new MenuAnimationController(this); + mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); + mTargetFeaturesView.setAccessibilityDelegateCompat( + new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) { + @NonNull + @Override + public AccessibilityDelegateCompat getItemDelegate() { + return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, + mMenuAnimationController); + } + }); setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); // Avoid drawing out of bounds of the parent view setClipToOutline(true); + loadLayoutResources(); addView(mTargetFeaturesView); } @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + inoutInfo.touchableRegion.set(mBoundsInParent); + } + + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); loadLayoutResources(); + + mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); + } + + void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { + mTargetFeaturesView.addOnItemTouchListener(listener); } @SuppressLint("NotifyDataSetChanged") @@ -80,11 +120,25 @@ class MenuView extends FrameLayout { } private void onSizeChanged() { + mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top, + mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(), + mBoundsInParent.top + mMenuViewAppearance.getMenuHeight()); + final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); layoutParams.height = mMenuViewAppearance.getMenuHeight(); setLayoutParams(layoutParams); } + void onEdgeChangedIfNeeded() { + final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds(); + if (getTranslationX() != draggableBounds.left + && getTranslationX() != draggableBounds.right) { + return; + } + + onEdgeChanged(); + } + private void onEdgeChanged() { final int[] insets = mMenuViewAppearance.getMenuInsets(); getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], @@ -96,8 +150,22 @@ class MenuView extends FrameLayout { mMenuViewAppearance.getMenuStrokeColor()); } + private void onPercentagePosition(Position percentagePosition) { + mMenuViewAppearance.setPercentagePosition(percentagePosition); + + onPositionChanged(); + } + + void onPositionChanged() { + final PointF position = mMenuViewAppearance.getMenuPosition(); + mMenuAnimationController.moveToPosition(position); + onBoundsInParentChanged((int) position.x, (int) position.y); + } + @SuppressLint("NotifyDataSetChanged") private void onSizeTypeChanged(int newSizeType) { + mMenuAnimationController.fadeInNowIfEnabled(); + mMenuViewAppearance.setSizeType(newSizeType); mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); @@ -106,41 +174,117 @@ class MenuView extends FrameLayout { onSizeChanged(); onEdgeChanged(); + onPositionChanged(); + + mMenuAnimationController.fadeOutIfEnabled(); } private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) { // TODO(b/252756133): Should update specific item instead of the whole list + mMenuAnimationController.fadeInNowIfEnabled(); + mTargetFeatures.clear(); mTargetFeatures.addAll(newTargetFeatures); mMenuViewAppearance.setTargetFeaturesSize(mTargetFeatures.size()); + mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); mAdapter.notifyDataSetChanged(); onSizeChanged(); onEdgeChanged(); + onPositionChanged(); + + mMenuAnimationController.fadeOutIfEnabled(); + } + + private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) { + mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(), + fadeEffectInfo.getOpacity()); + } + + Rect getMenuDraggableBounds() { + return mMenuViewAppearance.getMenuDraggableBounds(); + } + + void persistPositionAndUpdateEdge(Position percentagePosition) { + mMenuViewModel.updateMenuSavingPosition(percentagePosition); + mMenuViewAppearance.setPercentagePosition(percentagePosition); + + onEdgeChangedIfNeeded(); + } + + /** + * Uses the touch events from the parent view to identify if users clicked the extra + * space of the menu view. If yes, will use the percentage position and update the + * translations of the menu view to meet the effect of moving out from the edge. It’s only + * used when the menu view is hidden to the screen edge. + * + * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the + * {@link MenuView}. + * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the + * {@link MenuView}. + * @return true if consume the touch event, otherwise false. + */ + boolean maybeMoveOutEdgeAndShow(int x, int y) { + // Utilizes the touch region of the parent view to implement that users could tap extra + // the space region to show the menu from the edge. + if (!mMenuAnimationController.isMovedToEdge() || !mBoundsInParent.contains(x, y)) { + return false; + } + + mMenuAnimationController.fadeInNowIfEnabled(); + + mMenuAnimationController.moveOutEdgeAndShow(); + + mMenuAnimationController.fadeOutIfEnabled(); + return true; } void show() { + mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver); + mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver); mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver); mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver); setVisibility(VISIBLE); mMenuViewModel.registerContentObservers(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); } void hide() { setVisibility(GONE); + mBoundsInParent.setEmpty(); + mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver); + mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver); mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver); mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver); mMenuViewModel.unregisterContentObservers(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); + } + + void onDraggingStart() { + final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets(); + getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], + insets[3]); + + final GradientDrawable gradientDrawable = getContainerViewGradient(); + gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuMovingStateRadii()); + } + + void onBoundsInParentChanged(int newLeft, int newTop) { + mBoundsInParent.offsetTo(newLeft, newTop); } void loadLayoutResources() { mMenuViewAppearance.update(); + mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription()); setBackground(mMenuViewAppearance.getMenuBackground()); setElevation(mMenuViewAppearance.getMenuElevation()); onItemSizeChanged(); onSizeChanged(); onEdgeChanged(); + onPositionChanged(); } private InstantInsetLayerDrawable getContainerViewInsetLayer() { @@ -150,4 +294,9 @@ class MenuView extends FrameLayout { private GradientDrawable getContainerViewGradient() { return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM); } + + private void updateSystemGestureExcludeRects() { + final ViewGroup parentView = (ViewGroup) getParent(); + parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java index b9b7732605c0..034e96a13029 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java @@ -16,12 +16,21 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.view.View.OVER_SCROLL_ALWAYS; +import static android.view.View.OVER_SCROLL_NEVER; + import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL; import android.annotation.IntDef; import android.content.Context; import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.annotation.DimenRes; @@ -34,9 +43,13 @@ import java.lang.annotation.RetentionPolicy; * Provides the layout resources information of the {@link MenuView}. */ class MenuViewAppearance { + private final WindowManager mWindowManager; private final Resources mRes; + private final Position mPercentagePosition = new Position(/* percentageX= */ + 0f, /* percentageY= */ 0f); private int mTargetFeaturesSize; private int mSizeType; + private int mMargin; private int mSmallPadding; private int mLargePadding; private int mSmallIconSize; @@ -51,6 +64,7 @@ class MenuViewAppearance { private int mElevation; private float[] mRadii; private Drawable mBackgroundDrawable; + private String mContentDescription; @IntDef({ SMALL, @@ -62,13 +76,15 @@ class MenuViewAppearance { int LARGE = 1; } - MenuViewAppearance(Context context) { + MenuViewAppearance(Context context, WindowManager windowManager) { + mWindowManager = windowManager; mRes = context.getResources(); update(); } void update() { + mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin); mSmallPadding = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding); mLargePadding = @@ -81,7 +97,7 @@ class MenuViewAppearance { mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius); mSmallMultipleRadius = mRes.getDimensionPixelSize( R.dimen.accessibility_floating_menu_small_multiple_radius); - mRadii = createRadii(getMenuRadius(mTargetFeaturesSize)); + mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); mLargeSingleRadius = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius); mLargeMultipleRadius = mRes.getDimensionPixelSize( @@ -93,18 +109,59 @@ class MenuViewAppearance { final Drawable drawable = mRes.getDrawable(R.drawable.accessibility_floating_menu_background); mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable}); + mContentDescription = mRes.getString( + com.android.internal.R.string.accessibility_select_shortcut_menu_title); } void setSizeType(int sizeType) { mSizeType = sizeType; - mRadii = createRadii(getMenuRadius(mTargetFeaturesSize)); + mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); } void setTargetFeaturesSize(int targetFeaturesSize) { mTargetFeaturesSize = targetFeaturesSize; - mRadii = createRadii(getMenuRadius(targetFeaturesSize)); + mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize)); + } + + void setPercentagePosition(Position percentagePosition) { + mPercentagePosition.update(percentagePosition); + + mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); + } + + Rect getMenuDraggableBounds() { + final int margin = getMenuMargin(); + final Rect draggableBounds = getWindowAvailableBounds(); + + // Initializes start position for mapping the translation of the menu view. + final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + final Insets displayCutoutInsets = windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.displayCutout()); + draggableBounds.offset(-displayCutoutInsets.left, -displayCutoutInsets.top); + + draggableBounds.top += margin; + draggableBounds.right -= getMenuWidth(); + draggableBounds.bottom -= Math.min( + getWindowAvailableBounds().height() - draggableBounds.top, + calculateActualMenuHeight() + margin); + return draggableBounds; + } + + PointF getMenuPosition() { + final Rect draggableBounds = getMenuDraggableBounds(); + + return new PointF( + draggableBounds.left + + draggableBounds.width() * mPercentagePosition.getPercentageX(), + draggableBounds.top + + draggableBounds.height() * mPercentagePosition.getPercentageY()); + } + + String getContentDescription() { + return mContentDescription; } Drawable getMenuBackground() { @@ -115,20 +172,41 @@ class MenuViewAppearance { return mElevation; } + int getMenuWidth() { + return getMenuPadding() * 2 + getMenuIconSize(); + } + int getMenuHeight() { - return calculateActualMenuHeight(); + return Math.min(getWindowAvailableBounds().height() - mMargin * 2, + calculateActualMenuHeight()); } int getMenuIconSize() { return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize; } + private int getMenuMargin() { + return mMargin; + } + int getMenuPadding() { return mSizeType == SMALL ? mSmallPadding : mLargePadding; } int[] getMenuInsets() { - return new int[]{mInset, 0, 0, 0}; + final int left = isMenuOnLeftSide() ? mInset : 0; + final int right = isMenuOnLeftSide() ? 0 : mInset; + + return new int[]{left, 0, right, 0}; + } + + int[] getMenuMovingStateInsets() { + return new int[]{0, 0, 0, 0}; + } + + float[] getMenuMovingStateRadii() { + final float radius = getMenuRadius(mTargetFeaturesSize); + return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; } int getMenuStrokeWidth() { @@ -147,6 +225,14 @@ class MenuViewAppearance { return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount); } + int getMenuScrollMode() { + return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER; + } + + private boolean hasExceededMaxWindowHeight() { + return calculateActualMenuHeight() > getWindowAvailableBounds().height(); + } + @DimenRes private int getSmallSize(int itemCount) { return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius; @@ -157,8 +243,29 @@ class MenuViewAppearance { return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius; } - private static float[] createRadii(float radius) { - return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}; + private static float[] createRadii(boolean isMenuOnLeftSide, float radius) { + return isMenuOnLeftSide + ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f} + : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius}; + } + + private Rect getWindowAvailableBounds() { + final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + final Insets insets = windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + + final Rect bounds = new Rect(windowMetrics.getBounds()); + bounds.left += insets.left; + bounds.right -= insets.right; + bounds.top += insets.top; + bounds.bottom -= insets.bottom; + + return bounds; + } + + private boolean isMenuOnLeftSide() { + return mPercentagePosition.getPercentageX() < 0.5f; } private int calculateActualMenuHeight() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 4ea2f7799c30..5252519e9faf 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -19,6 +19,8 @@ package com.android.systemui.accessibility.floatingmenu; import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; +import android.view.MotionEvent; +import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -41,17 +43,27 @@ class MenuViewLayer extends FrameLayout { int MENU_VIEW = 0; } - MenuViewLayer(@NonNull Context context) { + MenuViewLayer(@NonNull Context context, WindowManager windowManager) { super(context); final MenuViewModel menuViewModel = new MenuViewModel(context); - final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context); + final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, + windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); addView(mMenuView, LayerIndex.MENU_VIEW); } @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) { + return true; + } + + return super.onInterceptTouchEvent(event); + } + + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 1e15a599f796..d2093c200ca2 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_ import android.content.Context; import android.graphics.PixelFormat; +import android.view.WindowInsets; import android.view.WindowManager; /** @@ -33,7 +34,7 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { MenuViewLayerController(Context context, WindowManager windowManager) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context); + mMenuViewLayer = new MenuViewLayer(context, windowManager); } @Override @@ -68,9 +69,10 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); + params.receiveInsetsIgnoringZOrder = true; params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; params.windowAnimations = android.R.style.Animation_Translucent; - + params.setFitInsetsTypes(WindowInsets.Type.navigationBars()); return params; } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java index c3ba43950b6e..e8a2b6e8767b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java @@ -33,6 +33,9 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged { private final MutableLiveData<List<AccessibilityTarget>> mTargetFeaturesData = new MutableLiveData<>(); private final MutableLiveData<Integer> mSizeTypeData = new MutableLiveData<>(); + private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData = + new MutableLiveData<>(); + private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>(); private final MenuInfoRepository mInfoRepository; MenuViewModel(Context context) { @@ -49,11 +52,30 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged { mSizeTypeData.setValue(newSizeType); } + @Override + public void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) { + mFadeEffectInfoData.setValue(fadeEffectInfo); + } + + void updateMenuSavingPosition(Position percentagePosition) { + mInfoRepository.updateMenuSavingPosition(percentagePosition); + } + + LiveData<Position> getPercentagePositionData() { + mInfoRepository.loadMenuPosition(mPercentagePositionData::setValue); + return mPercentagePositionData; + } + LiveData<Integer> getSizeTypeData() { mInfoRepository.loadMenuSizeType(mSizeTypeData::setValue); return mSizeTypeData; } + LiveData<MenuFadeEffectInfo> getFadeEffectInfoData() { + mInfoRepository.loadMenuFadeEffectInfo(mFadeEffectInfoData::setValue); + return mFadeEffectInfoData; + } + LiveData<List<AccessibilityTarget>> getTargetFeaturesData() { mInfoRepository.loadMenuTargetFeatures(mTargetFeaturesData::setValue); return mTargetFeaturesData; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java index 7b7eda84df4c..fc21be280dd6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import android.annotation.FloatRange; +import android.annotation.NonNull; import android.text.TextUtils; /** @@ -62,6 +63,13 @@ public class Position { } /** + * Updates the position with {@code percentagePosition}. + */ + public void update(@NonNull Position percentagePosition) { + update(percentagePosition.getPercentageX(), percentagePosition.getPercentageY()); + } + + /** * Updates the position with {@code percentageX} and {@code percentageY}. * * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 9493975ca00f..8c7e0efee7e6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -652,17 +652,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } - /** - * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps - * sensor area even if the user hasn't explicitly lifted their finger yet. - */ - public void onCancelUdfps() { - if (mUdfpsController == null) { - return; - } - mUdfpsController.onCancelUdfps(); - } - private void sendResultAndCleanUp(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { if (mReceiver == null) { @@ -1021,8 +1010,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } else { Log.w(TAG, "onBiometricError callback but dialog is gone"); } - - onCancelUdfps(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java index 5ed898682883..76cd3f4c4f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -24,6 +24,7 @@ import android.content.Context; import android.graphics.Insets; import android.os.UserHandle; import android.text.InputType; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; @@ -151,39 +152,52 @@ public class AuthCredentialPasswordView extends AuthCredentialView protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (mAuthCredentialInput == null || mAuthCredentialHeader == null - || mSubtitleView == null || mPasswordField == null || mErrorView == null) { + if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null + || mDescriptionView == null || mPasswordField == null || mErrorView == null) { return; } - // b/157910732 In AuthContainerView#getLayoutParams() we used to prevent jank risk when - // resizing by IME show or hide, we used to setFitInsetsTypes `~WindowInsets.Type.ime()` to - // LP. As a result this view needs to listen onApplyWindowInsets() and handle onLayout. int inputLeftBound; int inputTopBound; int headerRightBound = right; + int headerTopBounds = top; + final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() + : mSubtitleView.getBottom(); + final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom + : mDescriptionView.getBottom(); if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - inputTopBound = (bottom - (mPasswordField.getHeight() + mErrorView.getHeight())) / 2; + inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; inputLeftBound = (right - left) / 2; headerRightBound = inputLeftBound; + headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); } else { - inputTopBound = mSubtitleView.getBottom() + (bottom - mSubtitleView.getBottom()) / 2; + inputTopBound = + descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; } - mAuthCredentialHeader.layout(left, top, headerRightBound, bottom); + if (mDescriptionView.getBottom() > mBottomInset) { + mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); + } mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int newWidth = MeasureSpec.getSize(widthMeasureSpec); final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), newHeight); + setMeasuredDimension(newWidth, newHeight); - measureChildren(widthMeasureSpec, - MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.AT_MOST)); + final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, + MeasureSpec.AT_MOST); + final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec); + } else { + measureChildren(widthMeasureSpec, fullHeightSpec); + } } @NonNull @@ -193,6 +207,20 @@ public class AuthCredentialPasswordView extends AuthCredentialView final Insets bottomInset = insets.getInsets(ime()); if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { mBottomInset = bottomInset.bottom; + if (mBottomInset > 0 + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + mTitleView.setSingleLine(true); + mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mTitleView.setMarqueeRepeatLimit(-1); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); + } else { + mTitleView.setSingleLine(false); + mTitleView.setEllipsize(null); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(false); + } requestLayout(); } return insets; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java index 11498dbc0b83..f9e44a0c1724 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -93,7 +93,9 @@ public class AuthCredentialPatternView extends AuthCredentialView { @Override protected void onErrorTimeoutFinish() { super.onErrorTimeoutFinish(); - mLockPatternView.setEnabled(true); + // select to enable marquee unless a screen reader is enabled + mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); } public AuthCredentialPatternView(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index d4176acf10f9..fa623d146756 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -77,7 +77,7 @@ public abstract class AuthCredentialView extends LinearLayout { protected final Handler mHandler; protected final LockPatternUtils mLockPatternUtils; - private final AccessibilityManager mAccessibilityManager; + protected final AccessibilityManager mAccessibilityManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; @@ -86,10 +86,10 @@ public abstract class AuthCredentialView extends LinearLayout { private boolean mShouldAnimatePanel; private boolean mShouldAnimateContents; - private TextView mTitleView; + protected TextView mTitleView; protected TextView mSubtitleView; - private TextView mDescriptionView; - private ImageView mIconView; + protected TextView mDescriptionView; + protected ImageView mIconView; protected TextView mErrorView; protected @Utils.CredentialType int mCredentialType; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 0f5a99c1596d..3273d7429d49 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -788,7 +788,7 @@ public class UdfpsController implements DozeReceiver { // ACTION_UP/ACTION_CANCEL, we need to be careful about not letting the screen // accidentally remain in high brightness mode. As a mitigation, queue a call to // cancel the fingerprint scan. - mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps, + mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt, AOD_INTERRUPT_TIMEOUT_MILLIS); // using a hard-coded value for major and minor until it is available from the sensor onFingerDown(requestId, screenX, screenY, minor, major); @@ -815,26 +815,22 @@ public class UdfpsController implements DozeReceiver { } /** - * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly - * lifts their finger. Generally, this should be called on errors in the authentication flow. - * - * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give - * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually. + * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL + * events, which means the fingerprint gesture created by the AOD interrupt needs to be + * cancelled manually. * This should be called when authentication either succeeds or fails. Failing to cancel the * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical * sensors, this can result in illumination persisting for longer than necessary. */ - void onCancelUdfps() { + @VisibleForTesting + void cancelAodInterrupt() { if (!mIsAodInterruptActive) { return; } if (mOverlay != null && mOverlay.getOverlayView() != null) { onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView()); } - if (mCancelAodTimeoutAction != null) { - mCancelAodTimeoutAction.run(); - mCancelAodTimeoutAction = null; - } + mCancelAodTimeoutAction = null; mIsAodInterruptActive = false; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 66a521c30f47..7d0109686351 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -21,13 +21,18 @@ import android.annotation.UiThread import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect -import android.hardware.biometrics.BiometricOverlayConstants +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.os.Build import android.os.RemoteException +import android.provider.Settings import android.util.Log import android.util.RotationUtils import android.view.LayoutInflater @@ -38,6 +43,7 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator @@ -54,13 +60,16 @@ import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" +@VisibleForTesting +const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" + /** * Keeps track of the overlay state and UI resources associated with a single FingerprintService * request. This state can persist across configuration changes via the [show] and [hide] * methods. */ @UiThread -class UdfpsControllerOverlay( +class UdfpsControllerOverlay @JvmOverloads constructor( private val context: Context, fingerprintManager: FingerprintManager, private val inflater: LayoutInflater, @@ -82,7 +91,8 @@ class UdfpsControllerOverlay( @ShowReason val requestReason: Int, private val controllerCallback: IUdfpsOverlayControllerCallback, private val onTouch: (View, MotionEvent, Boolean) -> Boolean, - private val activityLaunchAnimator: ActivityLaunchAnimator + private val activityLaunchAnimator: ActivityLaunchAnimator, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -102,18 +112,19 @@ class UdfpsControllerOverlay( gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or - WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY // Avoid announcing window title. accessibilityTitle = " " } /** A helper if the [requestReason] was due to enrollment. */ - val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) { - UdfpsEnrollHelper(context, fingerprintManager, requestReason) - } else { - null - } + val enrollHelper: UdfpsEnrollHelper? = + if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) { + UdfpsEnrollHelper(context, fingerprintManager, requestReason) + } else { + null + } /** If the overlay is currently showing. */ val isShowing: Boolean @@ -129,6 +140,17 @@ class UdfpsControllerOverlay( private var touchExplorationEnabled = false + private fun shouldRemoveEnrollmentUi(): Boolean { + if (isDebuggable) { + return Settings.Global.getInt( + context.contentResolver, + SETTING_REMOVE_ENROLLMENT_UI, + 0 /* def */ + ) != 0 + } + return false + } + /** Show the overlay or return false and do nothing if it is already showing. */ @SuppressLint("ClickableViewAccessibility") fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { @@ -183,7 +205,18 @@ class UdfpsControllerOverlay( view: UdfpsView, controller: UdfpsController ): UdfpsAnimationViewController<*>? { - return when (requestReason) { + val isEnrollment = when (requestReason) { + REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true + else -> false + } + + val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) { + REASON_AUTH_OTHER + } else { + requestReason + } + + return when (filteredRequestReason) { REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> { UdfpsEnrollViewController( @@ -198,7 +231,7 @@ class UdfpsControllerOverlay( overlayParams.scaleFactor ) } - BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> { + REASON_AUTH_KEYGUARD -> { UdfpsKeyguardViewController( view.addUdfpsView(R.layout.udfps_keyguard_view), statusBarStateController, @@ -216,7 +249,7 @@ class UdfpsControllerOverlay( activityLaunchAnimator ) } - BiometricOverlayConstants.REASON_AUTH_BP -> { + REASON_AUTH_BP -> { // note: empty controller, currently shows no visual affordance UdfpsBpViewController( view.addUdfpsView(R.layout.udfps_bp_view), @@ -226,8 +259,8 @@ class UdfpsControllerOverlay( dumpManager ) } - BiometricOverlayConstants.REASON_AUTH_OTHER, - BiometricOverlayConstants.REASON_AUTH_SETTINGS -> { + REASON_AUTH_OTHER, + REASON_AUTH_SETTINGS -> { UdfpsFpmOtherViewController( view.addUdfpsView(R.layout.udfps_fpm_other_view), statusBarStateController, @@ -440,4 +473,4 @@ private fun Int.isEnrollmentReason() = private fun Int.isImportantForAccessibility() = this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || - this == BiometricOverlayConstants.REASON_AUTH_BP + this == REASON_AUTH_BP diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index 3871248eccd5..858bac30880b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -44,9 +44,6 @@ public interface FalsingCollector { void onQsDown(); /** */ - void setQsExpanded(boolean expanded); - - /** */ boolean shouldEnforceBouncer(); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index 28aac051c66d..0b7d6ab5acf7 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -49,10 +49,6 @@ public class FalsingCollectorFake implements FalsingCollector { } @Override - public void setQsExpanded(boolean expanded) { - } - - @Override public boolean shouldEnforceBouncer() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index f5f9655ef24b..da3d293d543b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -23,6 +23,8 @@ import android.hardware.biometrics.BiometricSourceType; import android.util.Log; import android.view.MotionEvent; +import androidx.annotation.VisibleForTesting; + import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.SysUISingleton; @@ -30,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -133,6 +136,7 @@ class FalsingCollectorImpl implements FalsingCollector { ProximitySensor proximitySensor, StatusBarStateController statusBarStateController, KeyguardStateController keyguardStateController, + ShadeExpansionStateManager shadeExpansionStateManager, BatteryController batteryController, DockManager dockManager, @Main DelayableExecutor mainExecutor, @@ -157,6 +161,8 @@ class FalsingCollectorImpl implements FalsingCollector { mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); + mBatteryController.addCallback(mBatteryListener); mDockManager.addListener(mDockEventListener); } @@ -193,8 +199,8 @@ class FalsingCollectorImpl implements FalsingCollector { public void onQsDown() { } - @Override - public void setQsExpanded(boolean expanded) { + @VisibleForTesting + void onQsExpansionChanged(Boolean expanded) { if (expanded) { unregisterSensors(); } else if (mSessionStarted) { diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt index bebade0cc484..08e8293cbe9c 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or @@ -30,4 +31,20 @@ sealed class ContentDescription { data class Resource( @StringRes val res: Int, ) : ContentDescription() + + companion object { + /** + * Returns the loaded content description string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over + * this method. This should only be used for testing or concatenation purposes. + */ + fun ContentDescription?.loadContentDescription(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.description + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt index 5d0e08ffc307..4a5693202dba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference] @@ -31,4 +32,20 @@ sealed class Text { data class Resource( @StringRes val res: Int, ) : Text() + + companion object { + /** + * Returns the loaded test string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This + * should only be used for testing or concatenation purposes. + */ + fun Text?.loadText(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.text + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index d7638d663dc9..7e31626983e7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -23,7 +23,8 @@ import android.service.dreams.IDreamManager; import androidx.annotation.Nullable; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.clock.ClockModule; +import com.android.keyguard.clock.ClockInfoModule; +import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -120,7 +121,8 @@ import dagger.Provides; BiometricsModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, - ClockModule.class, + ClockInfoModule.class, + ClockRegistryModule.class, CoroutinesModule.class, DreamModule.class, ControlsModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index 5fdd198c19d7..c256e447056b 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -99,7 +99,7 @@ class FaceScanningProviderFactory @Inject constructor( } fun shouldShowFaceScanningAnim(): Boolean { - return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning + return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 2e51b51d2836..b69afeb37371 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -287,8 +287,8 @@ public class DozeLog implements Dumpable { /** * Appends sensor event dropped event to logs */ - public void traceSensorEventDropped(int sensorEvent, String reason) { - mLogger.logSensorEventDropped(sensorEvent, reason); + public void traceSensorEventDropped(@Reason int pulseReason, String reason) { + mLogger.logSensorEventDropped(pulseReason, reason); } /** @@ -386,6 +386,47 @@ public class DozeLog implements Dumpable { mLogger.logSetAodDimmingScrim((long) scrimOpacity); } + /** + * Appends sensor attempted to register and whether it was a successful registration. + */ + public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) { + mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered + * with a reason the sensor is being unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered, + String reason) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason); + } + + /** + * Appends the event of skipping a sensor registration since it's already registered. + */ + public void traceSkipRegisterSensor(String sensorInfo) { + mLogger.logSkipSensorRegistration(sensorInfo); + } + + /** + * Appends a plugin sensor was registered or unregistered event. + */ + public void tracePluginSensorUpdate(boolean registered) { + if (registered) { + mLogger.log("register plugin sensor"); + } else { + mLogger.log("unregister plugin sensor"); + } + } + private class SummaryStats { private int mCount; diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index 0e1bfba8aadb..18c8e01cbf76 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -25,6 +25,7 @@ import com.android.systemui.plugins.log.LogLevel.ERROR import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.log.dagger.DozeLog import com.android.systemui.statusbar.policy.DevicePostureController +import com.google.errorprone.annotations.CompileTimeConstant import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -224,10 +225,14 @@ class DozeLogger @Inject constructor( }) } - fun logPulseDropped(from: String, state: DozeMachine.State) { + /** + * Log why a pulse was dropped and the current doze machine state. The state can be null + * if the DozeMachine is the middle of transitioning between states. + */ + fun logPulseDropped(from: String, state: DozeMachine.State?) { buffer.log(TAG, INFO, { str1 = from - str2 = state.name + str2 = state?.name }, { "Pulse dropped, cannot pulse from=$str1 state=$str2" }) @@ -320,6 +325,50 @@ class DozeLogger @Inject constructor( "Doze car mode started" }) } + + fun logSensorRegisterAttempt(sensorInfo: String, successfulRegistration: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulRegistration + }, { + "Register sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt(sensorInfo: String, successfulUnregister: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + }, { + "Unregister sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt( + sensorInfo: String, + successfulUnregister: Boolean, + reason: String + ) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + str2 = reason + }, { + "Unregister sensor. reason=$str2. Success=$bool1 sensor=$str1" + }) + } + + fun logSkipSensorRegistration(sensor: String) { + buffer.log(TAG, DEBUG, { + str1 = sensor + }, { + "Skipping sensor registration because its already registered. sensor=$str1" + }) + } + + fun log(@CompileTimeConstant msg: String) { + buffer.log(TAG, DEBUG, msg) + } } private const val TAG = "DozeLog" diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 997a6e554364..d0258d37cc96 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -25,7 +25,6 @@ import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_ import android.annotation.AnyThread; import android.app.ActivityManager; -import android.content.Context; import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -40,7 +39,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.IndentingPrintWriter; -import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; @@ -91,12 +89,9 @@ import java.util.function.Consumer; * trigger callbacks on the provided {@link mProxCallback}. */ public class DozeSensors { - - private static final boolean DEBUG = DozeService.DEBUG; private static final String TAG = "DozeSensors"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); - private final Context mContext; private final AsyncSensorManager mSensorManager; private final AmbientDisplayConfiguration mConfig; private final WakeLock mWakeLock; @@ -147,7 +142,6 @@ public class DozeSensors { } DozeSensors( - Context context, AsyncSensorManager sensorManager, DozeParameters dozeParameters, AmbientDisplayConfiguration config, @@ -160,7 +154,6 @@ public class DozeSensors { AuthController authController, DevicePostureController devicePostureController ) { - mContext = context; mSensorManager = sensorManager; mConfig = config; mWakeLock = wakeLock; @@ -608,10 +601,7 @@ public class DozeSensors { // cancel the previous sensor: if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor); - if (DEBUG) { - Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] " - + rt); - } + mDozeLog.traceSensorUnregisterAttempt(oldSensor.toString(), rt, "posture changed"); mRegistered = false; } @@ -657,19 +647,13 @@ public class DozeSensors { if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) { if (!mRegistered) { mRegistered = mSensorManager.requestTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered); - } + mDozeLog.traceSensorRegisterAttempt(sensor.toString(), mRegistered); } else { - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered"); - } + mDozeLog.traceSkipRegisterSensor(sensor.toString()); } } else if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt); - } + mDozeLog.traceSensorUnregisterAttempt(sensor.toString(), rt); mRegistered = false; } } @@ -707,7 +691,6 @@ public class DozeSensors { final Sensor sensor = mSensors[mPosture]; mDozeLog.traceSensor(mPulseReason); mHandler.post(mWakeLock.wrap(() -> { - if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event)); if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) { UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP); } @@ -779,11 +762,11 @@ public class DozeSensors { && !mRegistered) { asyncSensorManager.registerPluginListener(mPluginSensor, this); mRegistered = true; - if (DEBUG) Log.d(TAG, "registerPluginListener"); + mDozeLog.tracePluginSensorUpdate(true /* registered */); } else if (mRegistered) { asyncSensorManager.unregisterPluginListener(mPluginSensor, this); mRegistered = false; - if (DEBUG) Log.d(TAG, "unregisterPluginListener"); + mDozeLog.tracePluginSensorUpdate(false /* registered */); } } @@ -816,10 +799,9 @@ public class DozeSensors { mHandler.post(mWakeLock.wrap(() -> { final long now = SystemClock.uptimeMillis(); if (now < mDebounceFrom + mDebounce) { - Log.d(TAG, "onSensorEvent dropped: " + triggerEventToString(event)); + mDozeLog.traceSensorEventDropped(mPulseReason, "debounce"); return; } - if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event)); mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues()); })); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index ef454ffbdeb1..32cb1c01b776 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -198,7 +198,7 @@ public class DozeTriggers implements DozeMachine.Part { mAllowPulseTriggers = true; mSessionTracker = sessionTracker; - mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters, + mDozeSensors = new DozeSensors(mSensorManager, dozeParameters, config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor, secureSettings, authController, devicePostureController); mDockManager = dockManager; @@ -536,13 +536,13 @@ public class DozeTriggers implements DozeMachine.Part { return; } - if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) { + if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) { if (!mAllowPulseTriggers) { mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers"); } else if (mDozeHost.isPulsePending()) { mDozeLog.tracePulseDropped("requestPulse - pulsePending"); - } else if (!canPulse()) { - mDozeLog.tracePulseDropped("requestPulse", dozeState); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState); } runIfNotNull(onPulseSuppressedListener); return; @@ -559,15 +559,16 @@ public class DozeTriggers implements DozeMachine.Part { // not in pocket, continue pulsing final boolean isPulsePending = mDozeHost.isPulsePending(); mDozeHost.setPulsePending(false); - if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) { + if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) { if (!isPulsePending) { mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer" + " pending, pulse was cancelled before it could start" + " transitioning to pulsing state."); } else if (mDozeHost.isPulsingBlocked()) { mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked"); - } else if (!canPulse()) { - mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState()); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("continuePulseRequest" + + " - doze state cannot pulse", dozeState); } runIfNotNull(onPulseSuppressedListener); return; @@ -582,10 +583,10 @@ public class DozeTriggers implements DozeMachine.Part { .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId())); } - private boolean canPulse() { - return mMachine.getState() == DozeMachine.State.DOZE - || mMachine.getState() == DozeMachine.State.DOZE_AOD - || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED; + private boolean canPulse(DozeMachine.State dozeState) { + return dozeState == DozeMachine.State.DOZE + || dozeState == DozeMachine.State.DOZE_AOD + || dozeState == DozeMachine.State.DOZE_AOD_DOCKED; } @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 478f86169718..609bd76cf210 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -24,8 +24,13 @@ import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL +import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.google.protobuf.nano.MessageNano +import java.io.BufferedOutputStream +import java.io.FileDescriptor +import java.io.FileOutputStream import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -100,7 +105,7 @@ class DumpHandler @Inject constructor( /** * Dump the diagnostics! Behavior can be controlled via [args]. */ - fun dump(pw: PrintWriter, args: Array<String>) { + fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { Trace.beginSection("DumpManager#dump()") val start = SystemClock.uptimeMillis() @@ -111,10 +116,12 @@ class DumpHandler @Inject constructor( return } - when (parsedArgs.dumpPriority) { - PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) - PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs) - else -> dumpParameterized(pw, parsedArgs) + when { + parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) + parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> { + dumpNormal(pw, parsedArgs) + } + else -> dumpParameterized(fd, pw, parsedArgs) } pw.println() @@ -122,7 +129,7 @@ class DumpHandler @Inject constructor( Trace.endSection() } - private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) { + private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { when (args.command) { "bugreport-critical" -> dumpCritical(pw, args) "bugreport-normal" -> dumpNormal(pw, args) @@ -130,7 +137,13 @@ class DumpHandler @Inject constructor( "buffers" -> dumpBuffers(pw, args) "config" -> dumpConfig(pw) "help" -> dumpHelp(pw) - else -> dumpTargets(args.nonFlagArgs, pw, args) + else -> { + if (args.proto) { + dumpProtoTargets(args.nonFlagArgs, fd, args) + } else { + dumpTargets(args.nonFlagArgs, pw, args) + } + } } } @@ -160,6 +173,26 @@ class DumpHandler @Inject constructor( } } + private fun dumpProtoTargets( + targets: List<String>, + fd: FileDescriptor, + args: ParsedArgs + ) { + val systemUIProto = SystemUIProtoDump() + if (targets.isNotEmpty()) { + for (target in targets) { + dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs) + } + } else { + dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs) + } + val buffer = BufferedOutputStream(FileOutputStream(fd)) + buffer.use { + it.write(MessageNano.toByteArray(systemUIProto)) + it.flush() + } + } + private fun dumpTargets( targets: List<String>, pw: PrintWriter, @@ -267,6 +300,7 @@ class DumpHandler @Inject constructor( } } } + PROTO -> pArgs.proto = true "-t", "--tail" -> { pArgs.tailLength = readArgument(iterator, arg) { it.toInt() @@ -278,6 +312,9 @@ class DumpHandler @Inject constructor( "-h", "--help" -> { pArgs.command = "help" } + // This flag is passed as part of the proto dump in Bug reports, we can ignore + // it because this is our default behavior. + "-a" -> {} else -> { throw ArgParseException("Unknown flag: $arg") } @@ -314,7 +351,7 @@ class DumpHandler @Inject constructor( const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_HIGH = "HIGH" const val PRIORITY_ARG_NORMAL = "NORMAL" - const val PROTO = "--sysui_proto" + const val PROTO = "--proto" } } @@ -338,6 +375,7 @@ private class ParsedArgs( var tailLength: Int = 0 var command: String? = null var listOnly = false + var proto = false } class ArgParseException(message: String) : Exception(message) diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index dbca65122fcb..ae780896a7e2 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,6 +18,8 @@ package com.android.systemui.dump import android.util.ArrayMap import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable +import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import javax.inject.Inject @@ -90,7 +92,7 @@ open class DumpManager @Inject constructor() { target: String, pw: PrintWriter, args: Array<String>, - tailLength: Int + tailLength: Int, ) { for (dumpable in dumpables.values) { if (dumpable.name.endsWith(target)) { @@ -107,6 +109,36 @@ open class DumpManager @Inject constructor() { } } + @Synchronized + fun dumpProtoTarget( + target: String, + protoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) { + dumpProtoDumpable(dumpable.dumpable, protoDump, args) + return + } + } + } + + @Synchronized + fun dumpProtoDumpables( + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable) { + dumpProtoDumpable( + dumpable.dumpable, + systemUIProtoDump, + args + ) + } + } + } + /** * Dumps all registered dumpables to [pw] */ @@ -184,6 +216,14 @@ open class DumpManager @Inject constructor() { buffer.dumpable.dump(pw, tailLength) } + private fun dumpProtoDumpable( + protoDumpable: ProtoDumpable, + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + protoDumpable.dumpProto(systemUIProtoDump, args) + } + private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean { val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable return existingDumpable == null || newDumpable == existingDumpable @@ -195,4 +235,4 @@ private data class RegisteredDumpable<T>( val dumpable: T ) -private const val TAG = "DumpManager"
\ No newline at end of file +private const val TAG = "DumpManager" diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java index 0a41a56b5ecb..da983ab03a1d 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java +++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java @@ -51,6 +51,7 @@ public class SystemUIAuxiliaryDumpService extends Service { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { // Simulate the NORMAL priority arg being passed to us mDumpHandler.dump( + fd, pw, new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL }); } diff --git a/packages/SystemUI/src/com/android/systemui/dump/sysui.proto b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto new file mode 100644 index 000000000000..cd8c08aeb2dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto3"; + +package com.android.systemui.dump; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto"; + +option java_multiple_files = true; + +message SystemUIProtoDump { + repeated com.android.systemui.qs.QsTileState tiles = 1; +} + diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java deleted file mode 100644 index 906153704076..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ /dev/null @@ -1,365 +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.flags; - -import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; - -import com.android.internal.annotations.Keep; -import com.android.systemui.R; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * List of {@link Flag} objects for use in SystemUI. - * - * Flag Ids are integers. - * Ids must be unique. This is enforced in a unit test. - * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with - * a comment. This is purely for organizational purposes. - * - * On public release builds, flags will always return their default value. There is no way to - * change their value on release builds. - * - * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb. - */ -public class Flags { - public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1); - - /***************************************/ - // 100 - notification - public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = - new UnreleasedFlag(103); - - public static final UnreleasedFlag NSSL_DEBUG_LINES = - new UnreleasedFlag(105); - - public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION = - new UnreleasedFlag(106); - - public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = - new ResourceBooleanFlag(108, R.bool.config_notificationToContents); - - public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS = - new ReleasedFlag(109); - - public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD = - new UnreleasedFlag(110, true); - - public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); - - public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112, - false); - - public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true); - - public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true); - - public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true); - - // next id: 116 - - /***************************************/ - // 200 - keyguard/lockscreen - - // ** Flag retired ** - // public static final BooleanFlag KEYGUARD_LAYOUT = - // new BooleanFlag(200, true); - - public static final ReleasedFlag LOCKSCREEN_ANIMATIONS = - new ReleasedFlag(201); - - public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION = - new ReleasedFlag(202); - - public static final ResourceBooleanFlag CHARGING_RIPPLE = - new ResourceBooleanFlag(203, R.bool.flag_charging_ripple); - - public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER = - new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher); - - public static final ResourceBooleanFlag FACE_SCANNING_ANIM = - new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); - - public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207); - - /** - * Flag to enable the usage of the new bouncer data source. This is a refactor of and - * eventual replacement of KeyguardBouncer.java. - */ - public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208); - - /** - * Whether the user interactor and repository should use `UserSwitcherController`. - * - * <p>If this is {@code false}, the interactor and repo skip the controller and directly access - * the framework APIs. - */ - public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = - new UnreleasedFlag(210); - - /** - * Whether `UserSwitcherController` should use the user interactor. - * - * <p>When this is {@code true}, the controller does not directly access framework APIs. - * Instead, it goes through the interactor. - * - * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is - * {@code true} as it would created a cycle between controller -> interactor -> controller. - */ - public static final ReleasedFlag USER_CONTROLLER_USES_INTERACTOR = new ReleasedFlag(211); - - /** - * Whether the clock on a wide lock screen should use the new "stepping" animation for moving - * the digits when the clock moves. - */ - public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(212); - - /***************************************/ - // 300 - power menu - public static final ReleasedFlag POWER_MENU_LITE = - new ReleasedFlag(300); - - /***************************************/ - // 400 - smartspace - public static final ReleasedFlag SMARTSPACE_DEDUPING = - new ReleasedFlag(400); - - public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = - new ReleasedFlag(401); - - public static final ResourceBooleanFlag SMARTSPACE = - new ResourceBooleanFlag(402, R.bool.flag_smartspace); - - /***************************************/ - // 500 - quick settings - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_USER_SWITCHER = - new ReleasedFlag(500); - - public static final UnreleasedFlag COMBINED_QS_HEADERS = - new UnreleasedFlag(501, true); - - public static final ResourceBooleanFlag PEOPLE_TILE = - new ResourceBooleanFlag(502, R.bool.flag_conversations); - - public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT = - new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut); - - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504); - - public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true); - public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = - new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); - - public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507); - - /***************************************/ - // 600- status bar - public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER = - new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip); - - public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE = - new ReleasedFlag(603, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND = - new UnreleasedFlag(604, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND = - new UnreleasedFlag(605, false); - - /***************************************/ - // 700 - dialer/calls - public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP = - new ReleasedFlag(700); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE = - new ReleasedFlag(701); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = - new ReleasedFlag(702); - - /***************************************/ - // 800 - general visual/theme - public static final ResourceBooleanFlag MONET = - new ResourceBooleanFlag(800, R.bool.flag_monet); - - /***************************************/ - // 801 - region sampling - public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801); - - // 802 - wallpaper rendering - public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true); - - // 803 - screen contents translation - public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803); - - /***************************************/ - // 900 - media - public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900); - public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901); - public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903); - public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904); - public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905); - public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906); - public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907); - - // 1000 - dock - public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING = - new ReleasedFlag(1000); - public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001); - - public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = - new UnreleasedFlag(1002, /* teamfood= */ true); - - public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true); - - // 1100 - windowing - @Keep - public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS = - new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false); - - /** - * b/170163464: animate bubbles expanded view collapse with home gesture - */ - @Keep - public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE = - new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true); - - @Keep - public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING = - new DeviceConfigBooleanFlag(1102, "record_task_content", - NAMESPACE_WINDOW_MANAGER, false, true); - - @Keep - public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW = - new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false); - - @Keep - public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING = - new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false); - - @Keep - public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL = - new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false); - - @Keep - public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED = - new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false); - - @Keep - public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES = - new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false); - - @Keep - public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE = - new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true); - @Keep - public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP = - new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true); - - @Keep - public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM = - new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false); - - // 1200 - predictive back - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag( - 1200, "persist.wm.debug.predictive_back", true); - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag( - 1201, "persist.wm.debug.predictive_back_anim", false); - @Keep - public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = - new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false); - - public static final UnreleasedFlag NEW_BACK_AFFORDANCE = - new UnreleasedFlag(1203, false /* teamfood */); - - // 1300 - screenshots - - public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300); - public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301); - - // 1400 - columbus - public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400); - - // 1500 - chooser - public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); - - // 1600 - accessibility - public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = - new UnreleasedFlag(1600); - - // 1700 - clipboard - public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700); - - // Pay no attention to the reflection behind the curtain. - // ========================== Curtain ========================== - // | | - // | . . . . . . . . . . . . . . . . . . . | - private static Map<Integer, Flag<?>> sFlagMap; - static Map<Integer, Flag<?>> collectFlags() { - if (sFlagMap != null) { - return sFlagMap; - } - - Map<Integer, Flag<?>> flags = new HashMap<>(); - List<Field> flagFields = getFlagFields(); - - for (Field field : flagFields) { - try { - Flag<?> flag = (Flag<?>) field.get(null); - flags.put(flag.getId(), flag); - } catch (IllegalAccessException e) { - // no-op - } - } - - sFlagMap = flags; - - return sFlagMap; - } - - static List<Field> getFlagFields() { - Field[] fields = Flags.class.getFields(); - List<Field> result = new ArrayList<>(); - - for (Field field : fields) { - Class<?> t = field.getType(); - if (Flag.class.isAssignableFrom(t)) { - result.add(field); - } - } - - return result; - } - // | . . . . . . . . . . . . . . . . . . . | - // | | - // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ - -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt new file mode 100644 index 000000000000..9bd3cb17d66e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -0,0 +1,356 @@ +/* + * 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.flags + +import android.provider.DeviceConfig +import com.android.internal.annotations.Keep +import com.android.systemui.R +import java.lang.reflect.Field + +/** + * List of [Flag] objects for use in SystemUI. + * + * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be + * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This + * is purely for organizational purposes. + * + * On public release builds, flags will always return their default value. There is no way to change + * their value on release builds. + * + * See [FeatureFlagsDebug] for instructions on flipping the flags via adb. + */ +object Flags { + @JvmField val TEAMFOOD = UnreleasedFlag(1) + + // 100 - notification + // TODO(b/254512751): Tracking Bug + val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103) + + // TODO(b/254512732): Tracking Bug + @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105) + + // TODO(b/254512505): Tracking Bug + @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106) + + // TODO(b/254512624): Tracking Bug + @JvmField + val NOTIFICATION_DRAG_TO_CONTENTS = + ResourceBooleanFlag(108, R.bool.config_notificationToContents) + + // TODO(b/254512517): Tracking Bug + val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true) + + // TODO(b/254512538): Tracking Bug + val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true) + + // TODO(b/254512425): Tracking Bug + val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false) + + // TODO(b/254512731): Tracking Bug + @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true) + val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true) + val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true) + // next id: 116 + + // 200 - keyguard/lockscreen + // ** Flag retired ** + // public static final BooleanFlag KEYGUARD_LAYOUT = + // new BooleanFlag(200, true); + // TODO(b/254512713): Tracking Bug + @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201) + + // TODO(b/254512750): Tracking Bug + val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202) + val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple) + + // TODO(b/254512281): Tracking Bug + @JvmField + val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher) + + // TODO(b/254512694): Tracking Bug + val FACE_SCANNING_ANIM = ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation) + + // TODO(b/254512676): Tracking Bug + @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true) + + /** + * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual + * replacement of KeyguardBouncer.java. + */ + // TODO(b/254512385): Tracking Bug + @JvmField val MODERN_BOUNCER = UnreleasedFlag(208) + + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + * If this is `false`, the interactor and repo skip the controller and directly access the + * framework APIs. + */ + // TODO(b/254513286): Tracking Bug + val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210) + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + * When this is `true`, the controller does not directly access framework APIs. Instead, it goes + * through the interactor. + * + * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it + * would created a cycle between controller -> interactor -> controller. + */ + // TODO(b/254513102): Tracking Bug + val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211) + + /** + * Whether the clock on a wide lock screen should use the new "stepping" animation for moving + * the digits when the clock moves. + */ + @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212) + + // 300 - power menu + // TODO(b/254512600): Tracking Bug + @JvmField val POWER_MENU_LITE = ReleasedFlag(300) + + // 400 - smartspace + + // TODO(b/254513100): Tracking Bug + val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401) + val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace) + + // 500 - quick settings + @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500) + + // TODO(b/254512321): Tracking Bug + @JvmField val COMBINED_QS_HEADERS = UnreleasedFlag(501, teamfood = true) + val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations) + @JvmField + val QS_USER_DETAIL_SHORTCUT = + ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut) + + // TODO(b/254512699): Tracking Bug + @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504) + + // TODO(b/254512747): Tracking Bug + val NEW_HEADER = UnreleasedFlag(505, teamfood = true) + + // TODO(b/254512383): Tracking Bug + @JvmField + val FULL_SCREEN_USER_SWITCHER = + ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher) + + // TODO(b/254512678): Tracking Bug + @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507) + + // 600- status bar + // TODO(b/254513246): Tracking Bug + val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip) + + // TODO(b/254513025): Tracking Bug + val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false) + + // TODO(b/254512623): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false) + + // TODO(b/254512660): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false) + + val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false) + + val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false) + + // 700 - dialer/calls + // TODO(b/254512734): Tracking Bug + val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700) + + // TODO(b/254512681): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701) + + // TODO(b/254512753): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702) + + // 800 - general visual/theme + @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet) + + // 801 - region sampling + // TODO(b/254512848): Tracking Bug + val REGION_SAMPLING = UnreleasedFlag(801) + + // 802 - wallpaper rendering + // TODO(b/254512923): Tracking Bug + @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true) + + // 803 - screen contents translation + // TODO(b/254513187): Tracking Bug + val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803) + + // 804 - monochromatic themes + @JvmField val MONOCHROMATIC_THEMES = UnreleasedFlag(804) + + // 900 - media + // TODO(b/254512697): Tracking Bug + val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900) + + // TODO(b/254512502): Tracking Bug + val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901) + + // TODO(b/254512726): Tracking Bug + val MEDIA_NEARBY_DEVICES = ReleasedFlag(903) + + // TODO(b/254512695): Tracking Bug + val MEDIA_MUTE_AWAIT = ReleasedFlag(904) + + // TODO(b/254512654): Tracking Bug + @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905) + + // TODO(b/254512673): Tracking Bug + @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906) + + // TODO(b/254513168): Tracking Bug + val UMO_SURFACE_RIPPLE = UnreleasedFlag(907) + + // 1000 - dock + val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000) + + // TODO(b/254512444): Tracking Bug + @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001) + + // TODO(b/254512758): Tracking Bug + @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002) + + // TODO(b/254512525): Tracking Bug + @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true) + + // 1100 - windowing + @Keep + val WM_ENABLE_SHELL_TRANSITIONS = + SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false) + + /** b/170163464: animate bubbles expanded view collapse with home gesture */ + @Keep + val BUBBLES_HOME_GESTURE = + SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true) + + // TODO(b/254513207): Tracking Bug + @JvmField + @Keep + val WM_ENABLE_PARTIAL_SCREEN_SHARING = + DeviceConfigBooleanFlag( + 1102, + "record_task_content", + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + false, + teamfood = true + ) + + // TODO(b/254512674): Tracking Bug + @JvmField + @Keep + val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false) + + @Keep + val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false) + + @Keep + val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false) + + @Keep + val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false) + + @Keep + val SHOW_FLOATING_TASKS_AS_BUBBLES = + SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false) + + @Keep + val ENABLE_FLING_TO_DISMISS_BUBBLE = + SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true) + + @Keep + val ENABLE_FLING_TO_DISMISS_PIP = + SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true) + + @Keep + val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = + SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false) + + // 1200 - predictive back + @Keep + val WM_ENABLE_PREDICTIVE_BACK = + SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true) + + @Keep + val WM_ENABLE_PREDICTIVE_BACK_ANIM = + SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false) + + @Keep + val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = + SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false) + + // TODO(b/254512728): Tracking Bug + @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false) + + // 1300 - screenshots + // TODO(b/254512719): Tracking Bug + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300) + + // TODO(b/254513155): Tracking Bug + @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301) + + // 1400 - columbus + // TODO(b/254512756): Tracking Bug + val QUICK_TAP_IN_PCC = ReleasedFlag(1400) + + // 1500 - chooser + // TODO(b/254512507): Tracking Bug + val CHOOSER_UNBUNDLED = UnreleasedFlag(1500) + + // 1600 - accessibility + @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600) + + // 1700 - clipboard + @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700) + + // 1800 - shade container + @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + + // Pay no attention to the reflection behind the curtain. + // ========================== Curtain ========================== + // | | + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + fun collectFlags(): Map<Int, Flag<*>> { + return flagFields + .map { field -> + // field[null] returns the current value of the field. + // See java.lang.Field#get + val flag = field[null] as Flag<*> + flag.id to flag + } + .toMap() + } + + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + val flagFields: List<Field> + get() { + return Flags::class.java.fields.filter { f -> + Flag::class.java.isAssignableFrom(f.type) + } + } + // | | + // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index da5819a7f3bc..3ef5499237f1 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -116,6 +116,7 @@ import com.android.systemui.MultiListLayout; import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.animation.Interpolators; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -448,10 +449,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * * @param keyguardShowing True if keyguard is showing * @param isDeviceProvisioned True if device is provisioned - * @param view The view from which we should animate the dialog when showing it + * @param expandable The expandable from which we should animate the dialog when + * showing it */ public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, - @Nullable View view) { + @Nullable Expandable expandable) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; if (mDialog != null && mDialog.isShowing()) { @@ -463,7 +465,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mDialog.dismiss(); mDialog = null; } else { - handleShow(view); + handleShow(expandable); } } @@ -495,7 +497,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } - protected void handleShow(@Nullable View view) { + protected void handleShow(@Nullable Expandable expandable) { awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); @@ -507,10 +509,12 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); - if (view != null) { - mDialogLaunchAnimator.showFromView(mDialog, view, - new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController( + new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)) : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } @@ -1016,8 +1020,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Log.w(TAG, "Bugreport handler could not be launched"); mIActivityManager.requestInteractiveBugReport(); } - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent( + CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } } @@ -1036,8 +1041,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); mIActivityManager.requestFullBugReport(); - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } return false; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 84bd8cec51c8..0d74dc850dda 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -401,6 +401,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final float mWindowCornerRadius; /** + * The duration in milliseconds of the dream open animation. + */ + private final int mDreamOpenAnimationDuration; + + /** * The animation used for hiding keyguard. This is used to fetch the animation timings if * WindowManager is not providing us with them. */ @@ -751,6 +756,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "keyguardGone"); mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false); mKeyguardDisplayManager.hide(); + mUpdateMonitor.startBiometricWatchdog(); Trace.endSection(); } @@ -946,8 +952,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f); - // Use the same duration as for the UNOCCLUDE. - mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION); + mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration); mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR); mOccludeByDreamAnimator.addUpdateListener( animation -> { @@ -1179,6 +1184,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mPowerButtonY = context.getResources().getDimensionPixelSize( R.dimen.physical_power_button_center_screen_location_y); mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + + mDreamOpenAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_dreamOpenAnimationDuration); } public void userActivity() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a875..56a1f1ae936e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 45b668e609ea..b186ae0ceec4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,9 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** Observable for the [StatusBarState] */ + val statusBarState: Flow<StatusBarState> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -185,6 +189,24 @@ constructor( return keyguardStateController.isShowing } + override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -197,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 000000000000..e8532ecfdc37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.data.repository + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.annotation.FloatRange +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class KeyguardTransitionRepository @Inject constructor() { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = MutableStateFlow(TransitionStep()) + val transitions = _transitions.asStateFlow() + + /* Information about the active transition. */ + private var currentTransitionInfo: TransitionInfo? = null + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] + */ + private var updateTransitionId: UUID? = null + + /** + * Interactors that require information about changes between [KeyguardState]s will call this to + * register themselves for flowable [TransitionStep]s when that transition occurs. + */ + fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * Begin a transition from one state to another. The [info.from] must match + * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid + * unplanned transitions. + */ + fun startTransition(info: TransitionInfo): UUID? { + if (currentTransitionInfo != null) { + // Open questions: + // * Queue of transitions? buffer of 1? + // * Are transitions cancellable if a new one is triggered? + // * What validation does this need to do? + Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + return null + } + currentTransitionInfo?.animator?.cancel() + + currentTransitionInfo = info + info.animator?.let { animator -> + // An animator was provided, so use it to run the transition + animator.setFloatValues(0f, 1f) + val updateListener = + object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + emitTransition( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) + } + } + val adapter = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + Log.i(TAG, "Starting transition: $info") + emitTransition(info, 0f, TransitionState.STARTED) + } + override fun onAnimationCancel(animation: Animator) { + Log.i(TAG, "Cancelling transition: $info") + } + override fun onAnimationEnd(animation: Animator) { + Log.i(TAG, "Ending transition: $info") + emitTransition(info, 1f, TransitionState.FINISHED) + animator.removeListener(this) + animator.removeUpdateListener(updateListener) + } + } + animator.addListener(adapter) + animator.addUpdateListener(updateListener) + animator.start() + return@startTransition null + } + ?: run { + Log.i(TAG, "Starting transition (manual): $info") + emitTransition(info, 0f, TransitionState.STARTED) + + // No animator, so it's manual. Provide a mechanism to callback + updateTransitionId = UUID.randomUUID() + return@startTransition updateTransitionId + } + } + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) { + if (updateTransitionId != transitionId) { + Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + return + } + + if (currentTransitionInfo == null) { + Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") + return + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 000000000000..400376663f1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt index 7d4db37c6b0f..2af9318d92ec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt @@ -273,8 +273,8 @@ constructor( /** Tell the bouncer to start the pre hide animation. */ fun startDisappearAnimation(runnable: Runnable) { val finishRunnable = Runnable { - repository.setStartDisappearAnimation(null) runnable.run() + repository.setStartDisappearAnimation(null) } repository.setStartDisappearAnimation(finishRunnable) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 192919e32cf6..fc2269c6b01c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -38,7 +38,7 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing fun isKeyguardShowing(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 000000000000..b166681433a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set<TransitionInteractor>, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 000000000000..59bb22786917 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 000000000000..3c2a12e3836a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { + + private var transitionId: UUID? = null + + override fun start() { + scope.launch { + shadeRepository.shadeModel.sample( + combine( + keyguardTransitionRepository.transitions, + keyguardRepository.statusBarState, + ) { transitions, statusBarState -> + Pair(transitions, statusBarState) + } + ) { shadeModel, pair -> + val (transitions, statusBarState) = pair + + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is integrated + // into KeyguardTransitionRepository + val isOnLockscreen = + transitions.transitionState == TransitionState.FINISHED && + transitions.to == KeyguardState.LOCKSCREEN + if ( + isOnLockscreen && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 000000000000..74c542c0043f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 000000000000..a2a46d9e3a71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 000000000000..f66d5d3650c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 000000000000..bb953477583d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 000000000000..bfccf3fe076c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt new file mode 100644 index 000000000000..d8691c17f53d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 000000000000..688ec912aac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +data class TransitionStep( + val from: KeyguardState = KeyguardState.NONE, + val to: KeyguardState = KeyguardState.NONE, + val value: Float = 0f, // constrained [0.0, 1.0] + val transitionState: TransitionState = TransitionState.NONE, +) diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 00bf2104b7f2..5897087019f7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -43,7 +43,7 @@ public class LogModule { @SysUISingleton @DozeLog public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) { - return factory.create("DozeLog", 120); + return factory.create("DozeLog", 150); } /** Provides a logging buffer for all logs related to the data layer of notifications. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index 1ac2a078c8a0..be357ee0ff73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -182,8 +182,7 @@ class MediaProjectionAppSelectorActivity( override fun shouldGetOnlyDefaultActivities() = false - // TODO(b/240924732) flip the flag when the recents selector is ready - override fun shouldShowContentPreview() = false + override fun shouldShowContentPreview() = true override fun createContentPreviewView(parent: ViewGroup): ViewGroup = recentsViewController.createView(parent) diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index c3de94f28aea..0a6043793ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -21,6 +21,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -31,6 +33,23 @@ class MediaTttUtils { const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" /** + * Returns the information needed to display the icon in [Icon] form. + * + * See [getIconInfoFromPackageName]. + */ + fun getIconFromPackageName( + context: Context, + appPackageName: String?, + logger: MediaTttLogger, + ): Icon { + val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger) + return Icon.Loaded( + iconInfo.drawable, + ContentDescription.Loaded(iconInfo.contentDescription) + ) + } + + /** * Returns the information needed to display the icon. * * The information will either contain app name and icon of the app playing media, or a diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index c24b0307fcd1..6e596ee1f473 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -18,17 +18,12 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context -import android.media.MediaRoute2Info import android.util.Log -import android.view.View import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R -import com.android.systemui.plugins.FalsingManager +import com.android.systemui.common.shared.model.Text import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo -import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -38,6 +33,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator * @property stringResId the res ID of the string that should be displayed in the chip. Null if the * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. + * @property endItem the item that should be displayed in the end section of the chip. * @property timeout the amount of time this chip should display on the screen before it times out * and disappears. */ @@ -46,6 +42,7 @@ enum class ChipStateSender( val uiEvent: UiEventLogger.UiEventEnum, @StringRes val stringResId: Int?, val transferStatus: TransferStatus, + val endItem: SenderEndItem?, val timeout: Long = DEFAULT_TIMEOUT_MILLIS ) { /** @@ -58,6 +55,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -71,6 +69,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -82,6 +81,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -94,6 +94,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -105,36 +106,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED + ), + ), /** * A state representing that a transfer back to this device has been successfully completed. @@ -144,36 +122,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED + ), + ), /** A state representing that a transfer to the receiver device has failed. */ TRANSFER_TO_RECEIVER_FAILED( @@ -181,6 +136,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that a transfer back to this device has failed. */ @@ -189,6 +145,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that this device is far away from any receiver device. */ @@ -197,37 +154,27 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, stringResId = null, transferStatus = TransferStatus.TOO_FAR, - ); + // We shouldn't be displaying the chipbar anyway + endItem = null, + ) { + override fun getChipTextString(context: Context, otherDeviceName: String): Text { + // TODO(b/245610654): Better way to handle this. + throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + + "so its string should never be fetched") + } + }; /** * Returns a fully-formed string with the text that the chip should display. * + * Throws an NPE if [stringResId] is null. + * * @param otherDeviceName the name of the other device involved in the transfer. */ - fun getChipTextString(context: Context, otherDeviceName: String): String? { - if (stringResId == null) { - return null - } - return context.getString(stringResId, otherDeviceName) + open fun getChipTextString(context: Context, otherDeviceName: String): Text { + return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) } - /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar - * when undo is clicked. - * @param undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ - open fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? = null - companion object { /** * Returns the sender state enum associated with the given [displayState] from @@ -253,6 +200,26 @@ enum class ChipStateSender( } } +/** Represents the item that should be displayed in the end section of the chip. */ +sealed class SenderEndItem { + /** A loading icon should be displayed. */ + object Loading : SenderEndItem() + + /** An error icon should be displayed. */ + object Error : SenderEndItem() + + /** + * An undo button should be displayed. + * + * @property uiEventOnClick the UI event to log when this button is clicked. + * @property newState the state that should immediately be transitioned to. + */ + data class UndoButton( + val uiEventOnClick: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState val newState: Int, + ) : SenderEndItem() +} + // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 224303ac098c..edf759ddfd22 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -20,14 +20,20 @@ import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info import android.util.Log +import android.view.View +import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG import javax.inject.Inject @@ -107,7 +113,90 @@ constructor( chipbarCoordinator.removeView(removalReason) } else { displayedState = chipState - chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback)) + chipbarCoordinator.displayView( + createChipbarInfo( + chipState, + routeInfo, + undoCallback, + context, + logger, + ) + ) } } + + /** + * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. + */ + private fun createChipbarInfo( + chipStateSender: ChipStateSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + context: Context, + logger: MediaTttLogger, + ): ChipbarInfo { + val packageName = routeInfo.clientPackageName + val otherDeviceName = routeInfo.name.toString() + + return ChipbarInfo( + // Display the app's icon as the start icon + startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger), + text = chipStateSender.getChipTextString(context, otherDeviceName), + endItem = + when (chipStateSender.endItem) { + null -> null + is SenderEndItem.Loading -> ChipbarEndItem.Loading + is SenderEndItem.Error -> ChipbarEndItem.Error + is SenderEndItem.UndoButton -> { + if (undoCallback != null) { + getUndoButton( + undoCallback, + chipStateSender.endItem.uiEventOnClick, + chipStateSender.endItem.newState, + routeInfo, + ) + } else { + null + } + } + }, + vibrationEffect = chipStateSender.transferStatus.vibrationEffect, + ) + } + + /** + * Returns an undo button for the chip. + * + * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and + * this coordinator will transition to [newState]. + */ + private fun getUndoButton( + undoCallback: IUndoMediaTransferCallback, + uiEvent: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState newState: Int, + routeInfo: MediaRoute2Info, + ): ChipbarEndItem.Button { + val onClickListener = + View.OnClickListener { + uiEventLogger.logUndoClicked(uiEvent) + undoCallback.onUndoTriggered() + + // The external service should eventually send us a new TransferTriggered state, but + // but that may take too long to go through the binder and the user may be confused + // as to why the UI hasn't changed yet. So, we immediately change the UI here. + updateMediaTapToTransferSenderDisplay( + newState, + routeInfo, + // Since we're force-updating the UI, we don't have any [undoCallback] from the + // external service (and TransferTriggered states don't have undo callbacks + // anyway). + undoCallback = null, + ) + } + + return ChipbarEndItem.Button( + Text.Resource(R.string.media_transfer_undo), + onClickListener, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt index f15720df5245..b96380976dec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt @@ -16,16 +16,36 @@ package com.android.systemui.media.taptotransfer.sender -/** Represents the different possible transfer states that we could be in. */ -enum class TransferStatus { +import android.os.VibrationEffect + +/** + * Represents the different possible transfer states that we could be in and the vibration effects + * that come with updating transfer states. + * + * @property vibrationEffect an optional vibration effect when the transfer status is changed. + */ +enum class TransferStatus( + val vibrationEffect: VibrationEffect? = null, +) { /** The transfer hasn't started yet. */ - NOT_STARTED, + NOT_STARTED( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0) + .compose() + ), /** The transfer is currently ongoing but hasn't completed yet. */ - IN_PROGRESS, + IN_PROGRESS( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70) + .compose(), + ), /** The transfer has completed successfully. */ SUCCEEDED, /** The transfer has completed with a failure. */ - FAILED, + FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)), /** The device is too far away to do a transfer. */ TOO_FAR, } diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 482a1397642b..bb2b4419a80a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -52,6 +52,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -98,10 +99,10 @@ interface FgsManagerController { fun init() /** - * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if + * Show the foreground services dialog. The dialog will be expanded from [expandable] if * it's not `null`. */ - fun showDialog(viewLaunchedFrom: View?) + fun showDialog(expandable: Expandable?) /** Add a [OnNumberOfPackagesChangedListener]. */ fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) @@ -367,7 +368,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun shouldUpdateFooterVisibility() = dialog == null - override fun showDialog(viewLaunchedFrom: View?) { + override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { @@ -403,16 +404,18 @@ class FgsManagerControllerImpl @Inject constructor( } mainExecutor.execute { - viewLaunchedFrom - ?.let { - dialogLaunchAnimator.showFromView( - dialog, it, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG - ) + val controller = + expandable?.dialogLaunchController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, ) - } ?: dialog.show() + ) + if (controller != null) { + dialogLaunchAnimator.show(dialog, controller) + } else { + dialog.show() + } } backgroundExecutor.execute { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index 9d64781ef2e9..a9943e886339 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -32,6 +32,7 @@ import com.android.internal.logging.nano.MetricsProto import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -156,7 +157,7 @@ internal class FooterActionsController @Inject constructor( startSettingsActivity() } else if (v === powerMenuLite) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) - globalActionsDialog?.showOrHideDialog(false, true, v) + globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite)) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 7511278e0919..b1b9dd721eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -29,6 +29,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.dagger.QSScope; @@ -130,7 +131,7 @@ public class QSFgsManagerFooter implements View.OnClickListener, @Override public void onClick(View view) { - mFgsManagerController.showDialog(mRootView); + mFgsManagerController.showDialog(Expandable.fromView(view)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 67bf3003deff..6c1e95645550 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -39,6 +39,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; @@ -169,7 +170,7 @@ public class QSSecurityFooter extends ViewController<View> // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView); + mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index ae6ed2008a77..67bc76998597 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -75,6 +75,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.R; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.common.shared.model.ContentDescription; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.SysUISingleton; @@ -190,8 +191,9 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } /** Show the device monitoring dialog. */ - public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { - createDialog(quickSettingsContext, view); + public void showDeviceMonitoringDialog(Context quickSettingsContext, + @Nullable Expandable expandable) { + createDialog(quickSettingsContext, expandable); } /** @@ -440,7 +442,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } } - private void createDialog(Context quickSettingsContext, @Nullable View view) { + private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) { mShouldUseSettingsButton.set(false); mBgHandler.post(() -> { String settingsButtonText = getSettingsButton(); @@ -453,9 +455,12 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { ? settingsButtonText : getNegativeButton(), this); mDialog.setView(dialogView); - if (view != null && view.isAggregatedVisible()) { - mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)) + : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index ac46c85c10a4..f37d66877069 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -34,10 +34,12 @@ import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; +import com.android.systemui.ProtoDumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -48,6 +50,7 @@ import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.nano.QsTileState; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; @@ -59,16 +62,20 @@ import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.util.settings.SecureSettings; +import org.jetbrains.annotations.NotNull; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Provider; @@ -82,7 +89,7 @@ import javax.inject.Provider; * This class also provides the interface for adding/removing/changing tiles. */ @SysUISingleton -public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { +public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; @@ -671,4 +678,15 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mTiles.values().stream().filter(obj -> obj instanceof Dumpable) .forEach(o -> ((Dumpable) o).dump(pw, args)); } + + @Override + public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) { + List<QsTileState> data = mTiles.values().stream() + .map(QSTile::getState) + .map(TileStateToProtoKt::toProto) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + systemUIProtoDump.tiles = data.toArray(new QsTileState[0]); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt new file mode 100644 index 000000000000..2c8a5a4981d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.service.quicksettings.Tile +import android.text.TextUtils +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.nano.QsTileState +import com.android.systemui.util.nano.ComponentNameProto + +fun QSTile.State.toProto(): QsTileState? { + if (TextUtils.isEmpty(spec)) return null + val state = QsTileState() + if (spec.startsWith(CustomTile.PREFIX)) { + val protoComponentName = ComponentNameProto() + val tileComponentName = CustomTile.getComponentFromSpec(spec) + protoComponentName.packageName = tileComponentName.packageName + protoComponentName.className = tileComponentName.className + state.componentName = protoComponentName + } else { + state.spec = spec + } + state.state = + when (this.state) { + Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE + Tile.STATE_INACTIVE -> QsTileState.INACTIVE + Tile.STATE_ACTIVE -> QsTileState.ACTIVE + else -> QsTileState.UNAVAILABLE + } + label?.let { state.label = it.toString() } + secondaryLabel?.let { state.secondaryLabel = it.toString() } + if (this is QSTile.BooleanState) { + state.booleanState = value + } + return state +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index cf9b41c25388..9ba3501c3434 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -23,13 +23,11 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import android.provider.Settings -import android.view.View import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.logging.nano.MetricsProto import com.android.internal.util.FrameworkStatsLog -import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton @@ -74,37 +72,27 @@ interface FooterActionsInteractor { val deviceMonitoringDialogRequests: Flow<Unit> /** - * Show the device monitoring dialog, expanded from [view]. - * - * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment] - * [com.android.systemui.qs.QSFragment]. - */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(view: View) - - /** - * Show the device monitoring dialog. + * Show the device monitoring dialog, expanded from [expandable] if it's not null. * * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings * fragment][com.android.systemui.qs.QSFragment]. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(quickSettingsContext: Context) + fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?) /** Show the foreground services dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showForegroundServicesDialog(view: View) + fun showForegroundServicesDialog(expandable: Expandable) /** Show the power menu dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) + fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) /** Show the settings. */ fun showSettings(expandable: Expandable) /** Show the user switcher. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showUserSwitcher(view: View) + fun showUserSwitcher(context: Context, expandable: Expandable) } @SysUISingleton @@ -147,28 +135,32 @@ constructor( null, ) - override fun showDeviceMonitoringDialog(view: View) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view) - DevicePolicyEventLogger.createEvent( - FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED - ) - .write() - } - - override fun showDeviceMonitoringDialog(quickSettingsContext: Context) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null) + override fun showDeviceMonitoringDialog( + quickSettingsContext: Context, + expandable: Expandable?, + ) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable) + if (expandable != null) { + DevicePolicyEventLogger.createEvent( + FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED + ) + .write() + } } - override fun showForegroundServicesDialog(view: View) { - fgsManagerController.showDialog(view) + override fun showForegroundServicesDialog(expandable: Expandable) { + fgsManagerController.showDialog(expandable) } - override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + override fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) globalActionsDialogLite.showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -189,21 +181,21 @@ constructor( ) } - override fun showUserSwitcher(view: View) { + override fun showUserSwitcher(context: Context, expandable: Expandable) { if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(view) + userSwitchDialogController.showDialog(context, expandable) return } val intent = - Intent(view.context, UserSwitcherActivity::class.java).apply { + Intent(context, UserSwitcherActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) } activityStarter.startActivity( intent, true /* dismissShade */, - ActivityLaunchAnimator.Controller.fromView(view, null), + expandable.activityLaunchController(), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt index dd1ffcc9fa12..3e39c8ee62f1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R +import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.people.ui.view.PeopleViewBinder.bind @@ -125,7 +126,7 @@ object FooterActionsViewBinder { launch { viewModel.security.collect { security -> if (previousSecurity != security) { - bindSecurity(securityHolder, security) + bindSecurity(view.context, securityHolder, security) previousSecurity = security } } @@ -159,6 +160,7 @@ object FooterActionsViewBinder { } private fun bindSecurity( + quickSettingsContext: Context, securityHolder: TextButtonViewHolder, security: FooterActionsSecurityButtonViewModel?, ) { @@ -171,9 +173,12 @@ object FooterActionsViewBinder { // Make sure that the chevron is visible and that the button is clickable if there is a // listener. val chevron = securityHolder.chevron - if (security.onClick != null) { + val onClick = security.onClick + if (onClick != null) { securityView.isClickable = true - securityView.setOnClickListener(security.onClick) + securityView.setOnClickListener { + onClick(quickSettingsContext, Expandable.fromView(securityView)) + } chevron.isVisible = true } else { securityView.isClickable = false @@ -205,7 +210,9 @@ object FooterActionsViewBinder { foregroundServicesWithNumberView.isVisible = false foregroundServicesWithTextView.isVisible = true - foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithTextView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithTextHolder.text.text = foregroundServices.text foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges } else { @@ -213,7 +220,9 @@ object FooterActionsViewBinder { foregroundServicesWithTextView.isVisible = false foregroundServicesWithNumberView.visibility = View.VISIBLE - foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithNumberView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges @@ -229,7 +238,7 @@ object FooterActionsViewBinder { } buttonView.setBackgroundResource(model.background) - buttonView.setOnClickListener(model.onClick) + buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) } val icon = model.icon val iconView = button.icon diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt index 9b5f683d8dab..8d819dacba67 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.annotation.DrawableRes -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** @@ -29,7 +29,5 @@ data class FooterActionsButtonViewModel( val icon: Icon, val iconTint: Int?, @DrawableRes val background: Int, - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt index 98b53cb0ed5a..ff8130d3e6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import com.android.systemui.animation.Expandable /** A ViewModel for the foreground services button. */ data class FooterActionsForegroundServicesButtonViewModel( @@ -24,5 +24,5 @@ data class FooterActionsForegroundServicesButtonViewModel( val text: String, val displayText: Boolean, val hasNewChanges: Boolean, - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt index 98ab129fc9de..3450505f9f86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt @@ -16,12 +16,13 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import android.content.Context +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** A ViewModel for the security button. */ data class FooterActionsSecurityButtonViewModel( val icon: Icon, val text: String, - val onClick: ((View) -> Unit)?, + val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index d3c06f60bc90..dee6fadbc9cb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.content.Context import android.util.Log -import android.view.View import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -199,50 +198,51 @@ class FooterActionsViewModel( */ suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { footerActionsInteractor.deviceMonitoringDialogRequests.collect { - footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) + footerActionsInteractor.showDeviceMonitoringDialog( + quickSettingsContext, + expandable = null, + ) } } - private fun onSecurityButtonClicked(view: View) { + private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showDeviceMonitoringDialog(view) + footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } - private fun onForegroundServiceButtonClicked(view: View) { + private fun onForegroundServiceButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showForegroundServicesDialog(view) + footerActionsInteractor.showForegroundServicesDialog(expandable) } - private fun onUserSwitcherClicked(view: View) { + private fun onUserSwitcherClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showUserSwitcher(view) + footerActionsInteractor.showUserSwitcher(context, expandable) } - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - private fun onSettingsButtonClicked(view: View) { + private fun onSettingsButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showSettings(Expandable.fromView(view)) + footerActionsInteractor.showSettings(expandable) } - private fun onPowerButtonClicked(view: View) { + private fun onPowerButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) + footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) } private fun userSwitcherButton( diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto new file mode 100644 index 000000000000..2a61033cb302 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.systemui.qs; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto"; + +option java_multiple_files = true; + +message QsTileState { + oneof identifier { + string spec = 1; + com.android.systemui.util.ComponentNameProto component_name = 2; + } + + enum State { + UNAVAILABLE = 0; + INACTIVE = 1; + ACTIVE = 2; + } + + State state = 3; + oneof optional_boolean_state { + bool boolean_state = 4; + } + oneof optional_label { + string label = 5; + } + oneof optional_secondary_label { + string secondary_label = 6; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index bdcc6b0b2a57..314252bf310b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,13 +23,13 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import android.view.View import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -77,10 +77,10 @@ class UserSwitchDialogController @VisibleForTesting constructor( * Show a [UserDialog]. * * Populate the dialog with information from and adapter obtained from - * [userDetailViewAdapterProvider] and show it as launched from [view]. + * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ - fun showDialog(view: View) { - with(dialogFactory(view.context)) { + fun showDialog(context: Context, expandable: Expandable) { + with(dialogFactory(context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) @@ -112,13 +112,19 @@ class UserSwitchDialogController @VisibleForTesting constructor( adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) - dialogLaunchAnimator.showFromView( - this, view, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + val controller = + expandable.dialogLaunchController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) - ) + if (controller != null) { + dialogLaunchAnimator.show( + this, + controller, + ) + } else { + show() + } + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index fc93751b7f91..42e87530339d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -151,6 +151,8 @@ import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.media.MediaDataManager; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; +import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager.FalsingTapListener; @@ -583,6 +585,7 @@ public final class NotificationPanelViewController { private final SysUiState mSysUiState; private final NotificationShadeDepthController mDepthController; + private final NavigationBarController mNavigationBarController; private final int mDisplayId; private KeyguardIndicationController mKeyguardIndicationController; @@ -861,6 +864,7 @@ public final class NotificationPanelViewController { PrivacyDotViewController privacyDotViewController, TapAgainViewController tapAgainViewController, NavigationModeController navigationModeController, + NavigationBarController navigationBarController, FragmentService fragmentService, ContentResolver contentResolver, RecordingController recordingController, @@ -954,6 +958,7 @@ public final class NotificationPanelViewController { mNotificationsQSContainerController = notificationsQSContainerController; mNotificationListContainer = notificationListContainer; mNotificationStackSizeCalculator = notificationStackSizeCalculator; + mNavigationBarController = navigationBarController; mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider; mNotificationsQSContainerController.init(); mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; @@ -1443,6 +1448,16 @@ public final class NotificationPanelViewController { mMaxAllowedKeyguardNotifications = maxAllowed; } + @VisibleForTesting + boolean getClosing() { + return mClosing; + } + + @VisibleForTesting + boolean getIsFlinging() { + return mIsFlinging; + } + private void updateMaxDisplayedNotifications(boolean recompute) { if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); @@ -2671,12 +2686,16 @@ public final class NotificationPanelViewController { mQsExpanded = expanded; updateQsState(); updateExpandedHeightToMaxHeight(); - mFalsingCollector.setQsExpanded(expanded); - mCentralSurfaces.setQsExpanded(expanded); - mNotificationsQSContainerController.setQsExpanded(expanded); - mPulseExpansionHandler.setQsExpanded(expanded); - mKeyguardBypassController.setQSExpanded(expanded); - mPrivacyDotViewController.setQsExpanded(expanded); + setStatusAccessibilityImportance(expanded + ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + updateSystemUiStateFlags(); + NavigationBarView navigationBarView = + mNavigationBarController.getNavigationBarView(mDisplayId); + if (navigationBarView != null) { + navigationBarView.onStatusBarPanelStateChanged(); + } + mShadeExpansionStateManager.onQsExpansionChanged(expanded); } } @@ -3718,6 +3737,11 @@ public final class NotificationPanelViewController { setListening(true); } + @VisibleForTesting + void setTouchSlopExceeded(boolean isTouchSlopExceeded) { + mTouchSlopExceeded = isTouchSlopExceeded; + } + public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; @@ -3909,20 +3933,19 @@ public final class NotificationPanelViewController { mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); // Try triggering face auth, this "might" run. Check // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. - mUpdateMonitor.requestFaceAuth(true, + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); - mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, - 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); - mLockscreenGestureLogger - .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); - if (!mUpdateMonitor.isFaceDetectionRunning()) { - startUnlockHintAnimation(); - } - if (mUpdateMonitor.isFaceEnrolled()) { + if (didFaceAuthRun) { mUpdateMonitor.requestActiveUnlock( ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, "lockScreenEmptySpaceTap"); + } else { + mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, + 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); + mLockscreenGestureLogger + .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); + startUnlockHintAnimation(); } } return true; @@ -4776,6 +4799,7 @@ public final class NotificationPanelViewController { mAmbientState.setSwipingUp(false); if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop || Math.abs(y - mInitialExpandY) > mTouchSlop + || (!isFullyExpanded() && !isFullyCollapsed()) || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { mVelocityTracker.computeCurrentVelocity(1000); float vel = mVelocityTracker.getYVelocity(); @@ -5173,7 +5197,8 @@ public final class NotificationPanelViewController { */ public void updatePanelExpansionAndVisibility() { mShadeExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + mExpandedFraction, isExpanded(), + mTracking, mExpansionDragDownAmountPx); updateVisibility(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 1d9210592b78..66a22f4ddc0d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -135,7 +135,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW DumpManager dumpManager, KeyguardStateController keyguardStateController, ScreenOffAnimationController screenOffAnimationController, - AuthController authController) { + AuthController authController, + ShadeExpansionStateManager shadeExpansionStateManager) { mContext = context; mWindowManager = windowManager; mActivityManager = activityManager; @@ -156,6 +157,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW .addCallback(mStateListener, SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER); configurationController.addCallback(this); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); float desiredPreferredRefreshRate = context.getResources() .getInteger(R.integer.config_keyguardRefreshRate); @@ -607,8 +609,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW apply(mCurrentState); } - @Override - public void setQsExpanded(boolean expanded) { + private void onQsExpansionChanged(Boolean expanded) { mCurrentState.mQsExpanded = expanded; apply(mCurrentState); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index d6f0de83ecc1..73c6d507f035 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -36,17 +36,12 @@ class NotificationsQSContainerController @Inject constructor( private val navigationModeController: NavigationModeController, private val overviewProxyService: OverviewProxyService, private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController, + private val shadeExpansionStateManager: ShadeExpansionStateManager, private val featureFlags: FeatureFlags, @Main private val delayableExecutor: DelayableExecutor ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { - var qsExpanded = false - set(value) { - if (field != value) { - field = value - mView.invalidate() - } - } + private var qsExpanded = false private var splitShadeEnabled = false private var isQSDetailShowing = false private var isQSCustomizing = false @@ -71,6 +66,13 @@ class NotificationsQSContainerController @Inject constructor( taskbarVisible = visible } } + private val shadeQsExpansionListener: ShadeQsExpansionListener = + ShadeQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + mView.invalidate() + } + } // With certain configuration changes (like light/dark changes), the nav bar will disappear // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value @@ -106,6 +108,7 @@ class NotificationsQSContainerController @Inject constructor( public override fun onViewAttached() { updateResources() overviewProxyService.addCallback(taskbarVisibilityListener) + shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener) mView.setInsetsChangedListener(delayedInsetSetter) mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) } mView.setConfigurationChangedListener { updateResources() } @@ -113,6 +116,7 @@ class NotificationsQSContainerController @Inject constructor( override fun onViewDetached() { overviewProxyService.removeCallback(taskbarVisibilityListener) + shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener) mView.removeOnInsetsChangedListener() mView.removeQSFragmentAttachedListener() mView.setConfigurationChangedListener(null) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index f617d471351e..7bba74a8b125 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.annotation.FloatRange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject /** @@ -31,12 +32,14 @@ import javax.inject.Inject @SysUISingleton class ShadeExpansionStateManager @Inject constructor() { - private val expansionListeners = mutableListOf<ShadeExpansionListener>() - private val stateListeners = mutableListOf<ShadeStateListener>() + private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() + private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>() + private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false + private var qsExpanded: Boolean = false private var tracking: Boolean = false private var dragDownPxAmount: Float = 0f @@ -57,6 +60,15 @@ class ShadeExpansionStateManager @Inject constructor() { expansionListeners.remove(listener) } + fun addQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.add(listener) + listener.onQsExpansionChanged(qsExpanded) + } + + fun removeQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.remove(listener) + } + /** Adds a listener that will be notified when the panel state has changed. */ fun addStateListener(listener: ShadeStateListener) { stateListeners.add(listener) @@ -126,6 +138,14 @@ class ShadeExpansionStateManager @Inject constructor() { expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) } } + /** Called when the quick settings expansion changes to fully expanded or collapsed. */ + fun onQsExpansionChanged(qsExpanded: Boolean) { + this.qsExpanded = qsExpanded + + debugLog("qsExpanded=$qsExpanded") + qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) } + } + /** Updates the panel state if necessary. */ fun updateState(@PanelState state: Int) { debugLog( diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt new file mode 100644 index 000000000000..14882b9afd2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +/** A listener interface to be notified of expansion events for the quick settings panel. */ +fun interface ShadeQsExpansionListener { + /** + * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded + */ + fun onQsExpansionChanged(isQsExpanded: Boolean) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 000000000000..09019a69df47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow<ShadeModel> = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 000000000000..ce0f4283ff83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 9d4a27c64e5b..4ae0f6a4a6c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -67,12 +67,15 @@ import com.android.internal.statusbar.LetterboxDetails; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.GcUtils; import com.android.internal.view.AppearanceRegion; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.policy.CallbackController; import com.android.systemui.tracing.ProtoTracer; +import java.io.FileDescriptor; import java.io.FileOutputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; @@ -182,6 +185,7 @@ public class CommandQueue extends IStatusBar.Stub implements private int mLastUpdatedImeDisplayId = INVALID_DISPLAY; private ProtoTracer mProtoTracer; private final @Nullable CommandRegistry mRegistry; + private final @Nullable DumpHandler mDumpHandler; /** * These methods are called back on the main thread. @@ -471,12 +475,18 @@ public class CommandQueue extends IStatusBar.Stub implements } public CommandQueue(Context context) { - this(context, null, null); + this(context, null, null, null); } - public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) { + public CommandQueue( + Context context, + ProtoTracer protoTracer, + CommandRegistry registry, + DumpHandler dumpHandler + ) { mProtoTracer = protoTracer; mRegistry = registry; + mDumpHandler = dumpHandler; context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler); // We always have default display. setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE); @@ -1175,6 +1185,35 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void dumpProto(String[] args, ParcelFileDescriptor pfd) { + final FileDescriptor fd = pfd.getFileDescriptor(); + // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible + // to just throw this work onto the handler just like the other messages + Thread thr = new Thread("Sysui.dumpProto") { + public void run() { + try { + if (mDumpHandler == null) { + return; + } + // We won't be using the PrintWriter. + OutputStream o = new OutputStream() { + @Override + public void write(int b) {} + }; + mDumpHandler.dump(fd, new PrintWriter(o), args); + } finally { + try { + // Close the file descriptor so the TransferPipe finishes its thread + pfd.close(); + } catch (Exception e) { + } + } + } + }; + thr.start(); + } + + @Override public void runGcForTest() { // Gc sysui GcUtils.runGcAndFinalizersSync(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index 0c9e1ec1ff77..e21acb7e0f68 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -92,9 +92,6 @@ public interface NotificationShadeWindowController extends RemoteInputController /** Sets the state of whether the keyguard is fading away or not. */ default void setKeyguardFadingAway(boolean keyguardFadingAway) {} - /** Sets the state of whether the quick settings is expanded or not. */ - default void setQsExpanded(boolean expanded) {} - /** Sets the state of whether the user activities are forced or not. */ default void setForceUserActivity(boolean forceUserActivity) {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index 8222c9d9ba59..c630feba1dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -39,6 +39,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView @@ -68,6 +69,7 @@ constructor( configurationController: ConfigurationController, private val statusBarStateController: StatusBarStateController, private val falsingManager: FalsingManager, + shadeExpansionStateManager: ShadeExpansionStateManager, private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, private val falsingCollector: FalsingCollector, dumpManager: DumpManager @@ -126,6 +128,13 @@ constructor( initResources(context) } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + } + } + mPowerManager = context.getSystemService(PowerManager::class.java) dumpManager.registerDumpable(this) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 11e3d1773c4c..f574be056109 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -29,6 +29,7 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.ActivityStarter; @@ -181,8 +182,10 @@ public interface CentralSurfacesDependenciesModule { static CommandQueue provideCommandQueue( Context context, ProtoTracer protoTracer, - CommandRegistry registry) { - return new CommandQueue(context, protoTracer, registry); + CommandRegistry registry, + DumpHandler dumpHandler + ) { + return new CommandQueue(context, protoTracer, registry, dumpHandler); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index d88f07ca304c..737b4812d4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -25,11 +25,12 @@ import android.view.Gravity import android.view.View import android.widget.FrameLayout import com.android.internal.annotations.GuardedBy -import com.android.systemui.animation.Interpolators import com.android.systemui.R +import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener @@ -42,7 +43,6 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation - import java.util.concurrent.Executor import javax.inject.Inject @@ -67,7 +67,8 @@ class PrivacyDotViewController @Inject constructor( private val stateController: StatusBarStateController, private val configurationController: ConfigurationController, private val contentInsetsProvider: StatusBarContentInsetsProvider, - private val animationScheduler: SystemStatusAnimationScheduler + private val animationScheduler: SystemStatusAnimationScheduler, + shadeExpansionStateManager: ShadeExpansionStateManager ) { private lateinit var tl: View private lateinit var tr: View @@ -128,6 +129,13 @@ class PrivacyDotViewController @Inject constructor( updateStatusBarState() } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + dlog("setQsExpanded $isQsExpanded") + synchronized(lock) { + nextViewState = nextViewState.copy(qsExpanded = isQsExpanded) + } + } } fun setUiExecutor(e: DelayableExecutor) { @@ -138,13 +146,6 @@ class PrivacyDotViewController @Inject constructor( showingListener = l } - fun setQsExpanded(expanded: Boolean) { - dlog("setQsExpanded $expanded") - synchronized(lock) { - nextViewState = nextViewState.copy(qsExpanded = expanded) - } - } - @UiThread fun setNewRotation(rot: Int) { dlog("updateRotation: $rot") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 36b8333688ae..2734511de78c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -28,12 +28,7 @@ class NotifPipelineFlags @Inject constructor( fun isDevLoggingEnabled(): Boolean = featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING) - fun isSmartspaceDedupingEnabled(): Boolean = - featureFlags.isEnabled(Flags.SMARTSPACE) && - featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING) - - fun removeUnrankedNotifs(): Boolean = - featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS) + fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE) fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) 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 7242506f1015..d97b712df030 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification import android.animation.ObjectAnimator import android.util.FloatProperty +import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener @@ -32,17 +34,20 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min @SysUISingleton class NotificationWakeUpCoordinator @Inject constructor( + dumpManager: DumpManager, private val mHeadsUpManager: HeadsUpManager, private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController -) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener { +) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener, + Dumpable { private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( "notificationVisibility") { @@ -60,6 +65,7 @@ class NotificationWakeUpCoordinator @Inject constructor( private var mLinearDozeAmount: Float = 0.0f private var mDozeAmount: Float = 0.0f + private var mDozeAmountSource: String = "init" private var mNotificationVisibleAmount = 0.0f private var mNotificationsVisible = false private var mNotificationsVisibleForExpansion = false @@ -142,6 +148,7 @@ class NotificationWakeUpCoordinator @Inject constructor( } init { + dumpManager.registerDumpable(this) mHeadsUpManager.addListener(this) statusBarStateController.addCallback(this) addListener(object : WakeUpListener { @@ -248,13 +255,14 @@ class NotificationWakeUpCoordinator @Inject constructor( // Let's notify the scroller that an animation started notifyAnimationStart(mLinearDozeAmount == 1.0f) } - setDozeAmount(linear, eased) + setDozeAmount(linear, eased, source = "StatusBar") } - fun setDozeAmount(linear: Float, eased: Float) { + fun setDozeAmount(linear: Float, eased: Float, source: String) { val changed = linear != mLinearDozeAmount mLinearDozeAmount = linear mDozeAmount = eased + mDozeAmountSource = source mStackScrollerController.setDozeAmount(mDozeAmount) updateHideAmount() if (changed && linear == 0.0f) { @@ -271,7 +279,7 @@ class NotificationWakeUpCoordinator @Inject constructor( // undefined state, so it's an indication that we should do state cleanup. We override // the doze amount to 0f (not dozing) so that the notifications are no longer hidden. // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() - setDozeAmount(0f, 0f) + setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)") } if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { @@ -311,12 +319,11 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { - var amount = 1.0f - if (statusBarStateController.state == StatusBarState.SHADE || - statusBarStateController.state == StatusBarState.SHADE_LOCKED) { - amount = 0.0f + if (statusBarStateController.state == StatusBarState.KEYGUARD) { + setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)") + } else { + setDozeAmount(0f, 0f, source = "Override: bypass (shade)") } - setDozeAmount(amount, amount) return true } return false @@ -332,7 +339,7 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) { - setDozeAmount(1f, 1f) + setDozeAmount(1f, 1f, source = "Override: animating screen off") return true } @@ -414,6 +421,26 @@ class NotificationWakeUpCoordinator @Inject constructor( private fun shouldAnimateVisibility() = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("mLinearDozeAmount: $mLinearDozeAmount") + pw.println("mDozeAmount: $mDozeAmount") + pw.println("mDozeAmountSource: $mDozeAmountSource") + pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount") + pw.println("mNotificationsVisible: $mNotificationsVisible") + pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion") + pw.println("mVisibilityAmount: $mVisibilityAmount") + pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount") + pw.println("pulseExpanding: $pulseExpanding") + pw.println("state: ${StatusBarState.toString(state)}") + pw.println("fullyAwake: $fullyAwake") + pw.println("wakingUp: $wakingUp") + pw.println("willWakeUp: $willWakeUp") + pw.println("collapsedEnoughToHide: $collapsedEnoughToHide") + pw.println("pulsing: $pulsing") + pw.println("notificationsFullyHidden: $notificationsFullyHidden") + pw.println("canShowPulsingHuns: $canShowPulsingHuns") + } + interface WakeUpListener { /** * Called whenever the notifications are fully hidden or shown diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 2887f975d46c..df35c9e6832a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -602,7 +602,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { mInconsistencyTracker.logNewMissingNotifications(rankingMap); mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap); - if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) { + if (currentEntriesWithoutRankings != null) { for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { entry.mCancellationReason = REASON_UNKNOWN; tryRemoveNotification(entry); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8f3eb4f7e223..8a31ed9271ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap +import android.util.ArraySet +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -70,6 +72,7 @@ class HeadsUpCoordinator @Inject constructor( @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() + private val mEntriesUpdateTimes = ArrayMap<String, Long>() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 @@ -264,6 +267,9 @@ class HeadsUpCoordinator @Inject constructor( } // After this method runs, all posted entries should have been handled (or skipped). mPostedEntries.clear() + + // Also take this opportunity to clean up any stale entry update times + cleanUpEntryUpdateTimes() } /** @@ -378,6 +384,9 @@ class HeadsUpCoordinator @Inject constructor( isAlerting = false, isBinding = false, ) + + // Record the last updated time for this key + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -419,6 +428,9 @@ class HeadsUpCoordinator @Inject constructor( cancelHeadsUpBind(posted.entry) } } + + // Update last updated time for this entry + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -426,6 +438,7 @@ class HeadsUpCoordinator @Inject constructor( */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { mPostedEntries.remove(entry.key) + mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { @@ -454,7 +467,12 @@ class HeadsUpCoordinator @Inject constructor( // never) in mPostedEntries to need to alert, we need to check every notification // known to the pipeline. for (entry in mNotifPipeline.allNotifs) { - // The only entries we can consider alerting for here are entries that have never + // Only consider entries that are recent enough, since we want to apply a fairly + // strict threshold for when an entry should be updated via only ranking and not an + // app-provided notification update. + if (!isNewEnoughForRankingUpdate(entry)) continue + + // The only entries we consider alerting for here are entries that have never // interrupted and that now say they should heads up; if they've alerted in the // past, we don't want to incorrectly alert a second time if there wasn't an // explicit notification update. @@ -486,6 +504,41 @@ class HeadsUpCoordinator @Inject constructor( (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) } + /** + * Sets the updated time for the given entry to the specified time. + */ + @VisibleForTesting + fun setUpdateTime(entry: NotificationEntry, time: Long) { + mEntriesUpdateTimes[entry.key] = time + } + + /** + * Checks whether the entry is new enough to be updated via ranking update. + * We want to avoid updating an entry too long after it was originally posted/updated when we're + * only reacting to a ranking change, as relevant ranking updates are expected to come in + * fairly soon after the posting of a notification. + */ + private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean { + // If we don't have an update time for this key, default to "too old" + if (!mEntriesUpdateTimes.containsKey(entry.key)) return false + + val updateTime = mEntriesUpdateTimes[entry.key] ?: return false + return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS + } + + private fun cleanUpEntryUpdateTimes() { + // Because we won't update entries that are older than this amount of time anyway, clean + // up any entries that are too old to notify. + val toRemove = ArraySet<String>() + for ((key, updateTime) in mEntriesUpdateTimes) { + if (updateTime == null || + (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) { + toRemove.add(key) + } + } + mEntriesUpdateTimes.removeAll(toRemove) + } + /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> if (mNotifsExtendingLifetime.contains(entry)) { @@ -597,6 +650,9 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" private const val BIND_TIMEOUT = 1000L + + // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord. + private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000 } data class PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 6e76691ae1b1..d2db6224ef52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -407,7 +407,10 @@ public class PreparationCoordinator implements Coordinator { mLogger.logGroupInflationTookTooLong(group); return false; } - if (mInflatingNotifs.contains(group.getSummary())) { + // Only delay release if the summary is not inflated. + // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been + // done by this point, we can revert back to checking for mInflatingNotifs.contains(...) + if (group.getSummary() != null && !isInflated(group.getSummary())) { mLogger.logDelayingGroupRelease(group, group.getSummary()); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index c5a69217a1ac..c4f5a3a30608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.interruption; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR; import android.app.NotificationManager; import android.content.ContentResolver; @@ -32,6 +34,8 @@ import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -68,10 +72,30 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private final NotificationInterruptLogger mLogger; private final NotifPipelineFlags mFlags; private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + private final UiEventLogger mUiEventLogger; @VisibleForTesting protected boolean mUseHeadsUp = false; + public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior") + FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235), + + @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard") + FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236); + + private final int mId; + + NotificationInterruptEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + @Inject public NotificationInterruptStateProviderImpl( ContentResolver contentResolver, @@ -85,7 +109,8 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter NotificationInterruptLogger logger, @Main Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { mContentResolver = contentResolver; mPowerManager = powerManager; mDreamManager = dreamManager; @@ -97,6 +122,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter mLogger = logger; mFlags = flags; mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; + mUiEventLogger = uiEventLogger; ContentObserver headsUpObserver = new ContentObserver(mainHandler) { @Override public void onChange(boolean selfChange) { @@ -203,7 +229,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // b/231322873: Detect and report an event when a notification has both an FSI and a // suppressive groupAlertBehavior, and now correctly block the FSI from firing. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior"); + mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName); mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); return false; } @@ -249,7 +277,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // Detect the case determined by b/231322873 to launch FSI while device is in use, // as blocked by the correct implementation, and report the event. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard"); + mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName); mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt index 832a739a9080..0380fff1e2af 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -20,8 +20,9 @@ package com.android.systemui.statusbar.notification.logging /** Describes usage of a notification. */ data class NotificationMemoryUsage( val packageName: String, - val notificationId: String, + val notificationKey: String, val objectUsage: NotificationObjectUsage, + val viewUsage: List<NotificationViewUsage> ) /** @@ -39,3 +40,26 @@ data class NotificationObjectUsage( val extender: Int, val hasCustomView: Boolean, ) + +enum class ViewType { + PUBLIC_VIEW, + PRIVATE_CONTRACTED_VIEW, + PRIVATE_EXPANDED_VIEW, + PRIVATE_HEADS_UP_VIEW, + TOTAL +} + +/** + * Describes current memory of a notification view hierarchy. + * + * The values are in bytes. + */ +data class NotificationViewUsage( + val viewType: ViewType, + val smallIcon: Int, + val largeIcon: Int, + val systemIcons: Int, + val style: Int, + val customViews: Int, + val softwareBitmapsPenalty: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt new file mode 100644 index 000000000000..7d39e18ab349 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt @@ -0,0 +1,212 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.Person +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.WorkerThread +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */ +internal object NotificationMemoryMeter { + + private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" + private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" + private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" + private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" + private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + + /** Returns a list of memory use entries for currently shown notifications. */ + @WorkerThread + fun notificationMemoryUse( + notifications: Collection<NotificationEntry>, + ): List<NotificationMemoryUsage> { + return notifications + .asSequence() + .map { entry -> + val packageName = entry.sbn.packageName + val notificationObjectUsage = + notificationMemoryUse(entry.sbn.notification, hashSetOf()) + val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) + NotificationMemoryUsage( + packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationObjectUsage, + notificationViewUsage + ) + } + .toList() + } + + @WorkerThread + fun notificationMemoryUse( + entry: NotificationEntry, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationMemoryUsage { + return NotificationMemoryUsage( + entry.sbn.packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationMemoryUse(entry.sbn.notification, seenBitmaps), + NotificationMemoryViewWalker.getViewUsage(entry.row) + ) + } + + /** + * Computes the estimated memory usage of a given [Notification] object. It'll attempt to + * inspect Bitmaps in the object and provide summary of memory usage. + */ + @WorkerThread + fun notificationMemoryUse( + notification: Notification, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationObjectUsage { + val extras = notification.extras + val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) + val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) + + // Collect memory usage of extra styles + + // Big Picture + val bigPictureIconUse = + computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) + val bigPictureUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + // People + val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) + val peopleUse = + peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 + + // Calling + val callingPersonUse = + computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) + val verificationIconUse = + computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) + + // Messages + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES) + ) + val messagesUse = + messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + val historicMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + ) + val historyicMessagesUse = + historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + + // Extenders + val carExtender = extras.getBundle(CAR_EXTENSIONS) + val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 + val carExtenderIcon = + computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) + + val tvExtender = extras.getBundle(TV_EXTENSIONS) + val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 + + val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) + val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 + val wearExtenderBackground = + computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) + + val style = notification.notificationStyle + val hasCustomView = notification.contentView != null || notification.bigContentView != null + val extrasSize = computeBundleSize(extras) + + return NotificationObjectUsage( + smallIcon = smallIconUse, + largeIcon = largeIconUse, + extras = extrasSize, + style = style?.simpleName, + styleIcon = + bigPictureIconUse + + peopleUse + + callingPersonUse + + verificationIconUse + + messagesUse + + historyicMessagesUse, + bigPicture = bigPictureUse, + extender = + carExtenderSize + + carExtenderIcon + + tvExtenderSize + + wearExtenderSize + + wearExtenderBackground, + hasCustomView = hasCustomView + ) + } + + /** + * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem + * bitmaps). Can be slow. + */ + private fun computeBundleSize(extras: Bundle): Int { + val parcel = Parcel.obtain() + try { + extras.writeToParcel(parcel, 0) + return parcel.dataSize() + } finally { + parcel.recycle() + } + } + + /** + * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 + * if the key does not exist in extras. + */ + private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { + return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { + is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) + is Icon -> computeIconUse(parcelable, seenBitmaps) + is Person -> computeIconUse(parcelable.icon, seenBitmaps) + else -> 0 + } + } + + /** + * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is + * defined via Uri or a resource. + * + * @return memory usage in bytes or 0 if the icon is Uri/Resource based + */ + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = + when (icon?.type) { + Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) + else -> 0 + } + + /** + * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of + * seenBitmaps set, this method returns 0 to avoid double counting. + * + * @return memory usage of the bitmap in bytes + */ + private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { + val refId = System.identityHashCode(bitmap) + if (seenBitmaps?.contains(refId) == true) { + return 0 + } + + seenBitmaps?.add(refId) + return bitmap.allocationByteCount + } + + private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { + val refId = System.identityHashCode(icon.dataBytes) + if (seenBitmaps.contains(refId)) { + return 0 + } + + seenBitmaps.add(refId) + return icon.dataLength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt index 958978ecd858..c09cc4306ced 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -17,22 +17,11 @@ package com.android.systemui.statusbar.notification.logging -import android.app.Notification -import android.app.Person -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable import android.util.Log -import androidx.annotation.WorkerThread -import androidx.core.util.contains import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry import java.io.PrintWriter import javax.inject.Inject @@ -46,12 +35,7 @@ constructor( ) : Dumpable { companion object { - private const val TAG = "NotificationMemMonitor" - private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" - private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" - private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" - private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" - private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + private const val TAG = "NotificationMemory" } fun init() { @@ -60,184 +44,123 @@ constructor( } override fun dump(pw: PrintWriter, args: Array<out String>) { - currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) } + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) + .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) + dumpNotificationObjects(pw, memoryUse) + dumpNotificationViewUsage(pw, memoryUse) } - @WorkerThread - fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> { - return notificationMemoryUse(notificationPipeline.allNotifs) - } - - /** Returns a list of memory use entries for currently shown notifications. */ - @WorkerThread - fun notificationMemoryUse( - notifications: Collection<NotificationEntry> - ): List<NotificationMemoryUsage> { - return notifications - .asSequence() - .map { entry -> - val packageName = entry.sbn.packageName - val notificationObjectUsage = - computeNotificationObjectUse(entry.sbn.notification, hashSetOf()) - NotificationMemoryUsage( - packageName, - NotificationUtils.logKey(entry.sbn.key), - notificationObjectUsage - ) - } - .toList() - } - - /** - * Computes the estimated memory usage of a given [Notification] object. It'll attempt to - * inspect Bitmaps in the object and provide summary of memory usage. - */ - private fun computeNotificationObjectUse( - notification: Notification, - seenBitmaps: HashSet<Int> - ): NotificationObjectUsage { - val extras = notification.extras - val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) - val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) - - // Collect memory usage of extra styles - - // Big Picture - val bigPictureIconUse = - computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + - computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) - val bigPictureUse = - computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + - computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) - - // People - val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) - val peopleUse = - peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 - - // Calling - val callingPersonUse = - computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) - val verificationIconUse = - computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) - - // Messages - val messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - extras.getParcelableArray(Notification.EXTRA_MESSAGES) - ) - val messagesUse = - messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } - val historicMessages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + /** Renders a table of notification object usage into passed [PrintWriter]. */ + private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) { + pw.println("Notification Object Usage") + pw.println("-----------") + pw.println( + "Package".padEnd(35) + + "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" + ) + pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") + pw.println() + + memoryUse.forEach { use -> + pw.println( + use.packageName.padEnd(35) + + "\t\t" + + "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + + (use.objectUsage.style?.take(15) ?: "").padEnd(15) + + "\t\t${use.objectUsage.styleIcon}\t" + + "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + + "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + + use.notificationKey ) - val historyicMessagesUse = - historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } - - // Extenders - val carExtender = extras.getBundle(CAR_EXTENSIONS) - val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 - val carExtenderIcon = - computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) - - val tvExtender = extras.getBundle(TV_EXTENSIONS) - val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 - - val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) - val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 - val wearExtenderBackground = - computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) - - val style = notification.notificationStyle - val hasCustomView = notification.contentView != null || notification.bigContentView != null - val extrasSize = computeBundleSize(extras) - - return NotificationObjectUsage( - smallIconUse, - largeIconUse, - extrasSize, - style?.simpleName, - bigPictureIconUse + - peopleUse + - callingPersonUse + - verificationIconUse + - messagesUse + - historyicMessagesUse, - bigPictureUse, - carExtenderSize + - carExtenderIcon + - tvExtenderSize + - wearExtenderSize + - wearExtenderBackground, - hasCustomView + } + + // Calculate totals for easily glanceable summary. + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var styleIcon: Int = 0, + var bigPicture: Int = 0, + var extender: Int = 0, + var extras: Int = 0, ) - } - /** - * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem - * bitmaps). Can be slow. - */ - private fun computeBundleSize(extras: Bundle): Int { - val parcel = Parcel.obtain() - try { - extras.writeToParcel(parcel, 0) - return parcel.dataSize() - } finally { - parcel.recycle() - } - } + val totals = + memoryUse.fold(Totals()) { t, usage -> + t.smallIcon += usage.objectUsage.smallIcon + t.largeIcon += usage.objectUsage.largeIcon + t.styleIcon += usage.objectUsage.styleIcon + t.bigPicture += usage.objectUsage.bigPicture + t.extender += usage.objectUsage.extender + t.extras += usage.objectUsage.extras + t + } - /** - * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 - * if the key does not exist in extras. - */ - private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { - return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { - is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) - is Icon -> computeIconUse(parcelable, seenBitmaps) - is Person -> computeIconUse(parcelable.icon, seenBitmaps) - else -> 0 - } + pw.println() + pw.println("TOTALS") + pw.println( + "".padEnd(35) + + "\t\t" + + "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + + "".padEnd(15) + + "\t\t${toKb(totals.styleIcon)}\t" + + "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + + toKb(totals.extras) + ) + pw.println() } - /** - * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is - * defined via Uri or a resource. - * - * @return memory usage in bytes or 0 if the icon is Uri/Resource based - */ - private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = - when (icon?.type) { - Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) - Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) - Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) - else -> 0 - } - - /** - * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of - * seenBitmaps set, this method returns 0 to avoid double counting. - * - * @return memory usage of the bitmap in bytes - */ - private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { - val refId = System.identityHashCode(bitmap) - if (seenBitmaps?.contains(refId) == true) { - return 0 - } + /** Renders a table of notification view usage into passed [PrintWriter] */ + private fun dumpNotificationViewUsage( + pw: PrintWriter, + memoryUse: List<NotificationMemoryUsage>, + ) { + + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var style: Int = 0, + var customViews: Int = 0, + var softwareBitmapsPenalty: Int = 0, + ) - seenBitmaps?.add(refId) - return bitmap.allocationByteCount + val totals = Totals() + pw.println("Notification View Usage") + pw.println("-----------") + pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") + pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") + pw.println() + memoryUse + .filter { it.viewUsage.isNotEmpty() } + .forEach { use -> + pw.println(use.packageName + " " + use.notificationKey) + use.viewUsage.forEach { view -> + pw.println( + " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + + "\t${view.largeIcon}\t${view.style}" + + "\t${view.customViews}\t${view.softwareBitmapsPenalty}" + ) + + if (view.viewType == ViewType.TOTAL) { + totals.smallIcon += view.smallIcon + totals.largeIcon += view.largeIcon + totals.style += view.style + totals.customViews += view.customViews + totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty + } + } + } + pw.println() + pw.println("TOTALS") + pw.println( + " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + + "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + + "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" + ) + pw.println() } - private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { - val refId = System.identityHashCode(icon.dataBytes) - if (seenBitmaps.contains(refId)) { - return 0 - } - - seenBitmaps.add(refId) - return icon.dataLength + private fun toKb(bytes: Int): String { + return (bytes / 1024).toString() + " KB" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt new file mode 100644 index 000000000000..a0bee1502f51 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -0,0 +1,173 @@ +package com.android.systemui.statusbar.notification.logging + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.android.internal.R +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.util.children + +/** Walks view hiearchy of a given notification to estimate its memory use. */ +internal object NotificationMemoryViewWalker { + + private const val TAG = "NotificationMemory" + + /** Builder for [NotificationViewUsage] objects. */ + private class UsageBuilder { + private var smallIcon: Int = 0 + private var largeIcon: Int = 0 + private var systemIcons: Int = 0 + private var style: Int = 0 + private var customViews: Int = 0 + private var softwareBitmaps = 0 + + fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse } + fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse } + fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse } + fun addStyle(styleUse: Int) = apply { style += styleUse } + fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply { + softwareBitmaps += softwareBitmapUse + } + + fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse } + + fun build(viewType: ViewType): NotificationViewUsage { + return NotificationViewUsage( + viewType = viewType, + smallIcon = smallIcon, + largeIcon = largeIcon, + systemIcons = systemIcons, + style = style, + customViews = customViews, + softwareBitmapsPenalty = softwareBitmaps, + ) + } + } + + /** + * Returns memory usage of public and private views contained in passed + * [ExpandableNotificationRow] + */ + fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> { + if (row == null) { + return listOf() + } + + // The ordering here is significant since it determines deduplication of seen drawables. + return listOf( + getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), + getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), + getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), + getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), + getTotalUsage(row) + ) + } + + /** + * Calculate total usage of all views - we need to do a separate traversal to make sure we don't + * double count fields. + */ + private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { + val totalUsage = UsageBuilder() + val seenObjects = hashSetOf<Int>() + + row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } + row.privateLayout?.let { child -> + for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { + (view as? ViewGroup)?.let { v -> + computeViewHierarchyUse(v, totalUsage, seenObjects) + } + } + } + return totalUsage.build(ViewType.TOTAL) + } + + private fun getViewUsage( + type: ViewType, + rootView: View?, + seenObjects: HashSet<Int> = hashSetOf() + ): NotificationViewUsage { + val usageBuilder = UsageBuilder() + (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } + return usageBuilder.build(type) + } + + private fun computeViewHierarchyUse( + rootView: ViewGroup, + builder: UsageBuilder, + seenObjects: HashSet<Int> = hashSetOf(), + ) { + for (child in rootView.children) { + if (child is ViewGroup) { + computeViewHierarchyUse(child, builder, seenObjects) + } else { + computeViewUse(child, builder, seenObjects) + } + } + } + + private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) { + if (view !is ImageView) return + val drawable = view.drawable ?: return + val drawableRef = System.identityHashCode(drawable) + if (seenObjects.contains(drawableRef)) return + val drawableUse = computeDrawableUse(drawable, seenObjects) + // TODO(b/235451049): We need to make sure we traverse large icon before small icon - + // sometimes the large icons are assigned to small icon views and we want to + // attribute them to large view in those cases. + when (view.id) { + R.id.left_icon, + R.id.icon, + R.id.conversation_icon -> builder.addSmallIcon(drawableUse) + R.id.right_icon -> builder.addLargeIcon(drawableUse) + R.id.big_picture -> builder.addStyle(drawableUse) + // Elements that are part of platform with resources + R.id.phishing_alert, + R.id.feedback, + R.id.alerted_icon, + R.id.expand_button_icon, + R.id.remote_input_send -> builder.addSystem(drawableUse) + // Custom view ImageViews + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Custom view: ${identifierForView(view)}") + } + builder.addCustomViews(drawableUse) + } + } + + if (isDrawableSoftwareBitmap(drawable)) { + builder.addSoftwareBitmapPenalty(drawableUse) + } + + seenObjects.add(drawableRef) + } + + private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int = + when (drawable) { + is BitmapDrawable -> { + val ref = System.identityHashCode(drawable.bitmap) + if (seenObjects.contains(ref)) { + 0 + } else { + seenObjects.add(ref) + drawable.bitmap.allocationByteCount + } + } + else -> 0 + } + + private fun isDrawableSoftwareBitmap(drawable: Drawable) = + drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE + + private fun identifierForView(view: View) = + if (view.id == View.NO_ID) { + "no-id" + } else { + view.resources.getResourceName(view.id) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 25fd483efa2d..70cf56d6d12c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -262,8 +262,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn @Override void startActivity(Intent intent, boolean dismissShade, Callback callback); - void setQsExpanded(boolean expanded); - boolean isWakeUpComingFromTouch(); boolean isFalsingThresholdNeeded(); @@ -455,6 +453,9 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void collapseShade(); + /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */ + void collapseShadeForBugreport(); + int getWakefulnessState(); boolean isScreenFullyOff(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 2c834cf781a6..29642beda53d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -868,6 +868,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mBubblesOptional.get().setExpandListener(mBubbleExpandListener); } + // Do not restart System UI when the bugreport flag changes. + mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> { + event.requestNoRestart(); + }); + mStatusBarSignalPolicy.init(); mKeyguardIndicationController.init(); @@ -1772,18 +1777,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public void setQsExpanded(boolean expanded) { - mNotificationShadeWindowController.setQsExpanded(expanded); - mNotificationPanelViewController.setStatusAccessibilityImportance(expanded - ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - mNotificationPanelViewController.updateSystemUiStateFlags(); - if (getNavigationBarView() != null) { - getNavigationBarView().onStatusBarPanelStateChanged(); - } - } - - @Override public boolean isWakeUpComingFromTouch() { return mWakeUpComingFromTouch; } @@ -3561,6 +3554,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } + @Override + public void collapseShadeForBugreport() { + if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) { + collapseShade(); + } + } + @VisibleForTesting final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index b987f6815000..b965ac97cc1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -26,6 +26,7 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm @@ -95,14 +96,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr var bouncerShowing: Boolean = false var altBouncerShowing: Boolean = false var launchingAffordance: Boolean = false - var qSExpanded = false - set(value) { - val changed = field != value - field = value - if (changed && !value) { - maybePerformPendingUnlock() - } - } + var qsExpanded = false @Inject constructor( @@ -111,6 +105,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr statusBarStateController: StatusBarStateController, lockscreenUserManager: NotificationLockscreenUserManager, keyguardStateController: KeyguardStateController, + shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager ) { this.mKeyguardStateController = keyguardStateController @@ -132,6 +127,14 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr } }) + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + val changed = qsExpanded != isQsExpanded + qsExpanded = isQsExpanded + if (changed && !isQsExpanded) { + maybePerformPendingUnlock() + } + } + val dismissByDefault = if (context.resources.getBoolean( com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0 tunerService.addTunable(object : TunerService.Tunable { @@ -160,7 +163,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr ): Boolean { if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) { val can = canBypass() - if (!can && (isPulseExpanding || qSExpanded)) { + if (!can && (isPulseExpanding || qsExpanded)) { pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric) } return can @@ -189,7 +192,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr altBouncerShowing -> true statusBarStateController.state != StatusBarState.KEYGUARD -> false launchingAffordance -> false - isPulseExpanding || qSExpanded -> false + isPulseExpanding || qsExpanded -> false else -> true } } @@ -214,7 +217,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr pw.println(" altBouncerShowing: $altBouncerShowing") pw.println(" isPulseExpanding: $isPulseExpanding") pw.println(" launchingAffordance: $launchingAffordance") - pw.println(" qSExpanded: $qSExpanded") + pw.println(" qSExpanded: $qsExpanded") pw.println(" hasFaceFeature: $hasFaceFeature") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index 00c3e8fac0b4..5e2a7c8ca540 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -26,6 +26,7 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; @@ -67,7 +68,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { ActivityLaunchAnimator.Controller.fromView(v, null), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM); } else { - mUserSwitchDialogController.showDialog(v); + mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index ece7ee0ec98a..86f6ff850409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -372,7 +372,7 @@ public interface StatusBarIconController { mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); - if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (statusBarPipelineFlags.useNewMobileIcons()) { // This starts the flow for the new pipeline, and will notify us of changes mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); @@ -451,7 +451,7 @@ public interface StatusBarIconController { @VisibleForTesting protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) { final BaseStatusBarFrameLayout view; - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewWifiIcon()) { view = onCreateModernStatusBarWifiView(slot); // When [ModernStatusBarWifiView] is created, it will automatically apply the // correct view state so we don't need to call applyWifiState. @@ -474,9 +474,9 @@ public interface StatusBarIconController { String slot, MobileIconState state ) { - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { throw new IllegalStateException("Attempting to add a mobile icon while the new " - + "pipeline is enabled is not supported"); + + "icons are enabled is not supported"); } // Use the `subId` field as a key to query for the correct context @@ -497,7 +497,7 @@ public interface StatusBarIconController { String slot, int subId ) { - if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { throw new IllegalStateException("Attempting to add a mobile icon using the new" + "pipeline, but the enabled flag is false."); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index e106b9e327ef..31e960ad7d69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -224,9 +224,9 @@ public class StatusBarIconControllerImpl implements Tunable, */ @Override public void setMobileIcons(String slot, List<MobileIconState> iconStates) { - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { Log.d(TAG, "ignoring old pipeline callbacks, because the new " - + "pipeline frontend is enabled"); + + "icons are enabled"); return; } Slot mobileSlot = mStatusBarIconList.getSlot(slot); @@ -249,9 +249,9 @@ public class StatusBarIconControllerImpl implements Tunable, @Override public void setNewMobileIconSubIds(List<Integer> subIds) { - if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { Log.d(TAG, "ignoring new pipeline callback, " - + "since the frontend is disabled"); + + "since the new icons are disabled"); return; } Slot mobileSlot = mStatusBarIconList.getSlot("mobile"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 5f5ec68ba898..5480f5d7489e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -60,7 +60,6 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.BouncerView; -import com.android.systemui.keyguard.data.BouncerViewDelegate; import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; import com.android.systemui.navigationbar.NavigationBarView; @@ -136,7 +135,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController; private final BouncerCallbackInteractor mBouncerCallbackInteractor; private final BouncerInteractor mBouncerInteractor; - private final BouncerViewDelegate mBouncerViewDelegate; + private final BouncerView mBouncerView; private final Lazy<com.android.systemui.shade.ShadeController> mShadeController; private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() { @@ -327,7 +326,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardSecurityModel = keyguardSecurityModel; mBouncerCallbackInteractor = bouncerCallbackInteractor; mBouncerInteractor = bouncerInteractor; - mBouncerViewDelegate = bouncerView.getDelegate(); + mBouncerView = bouncerView; mFoldAodAnimationController = sysUIUnfoldComponent .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER); @@ -804,7 +803,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private void setDozing(boolean dozing) { if (mDozing != dozing) { mDozing = dozing; - if (dozing || mBouncer.needsFullscreenBouncer() + if (dozing || needsFullscreenBouncer() || mKeyguardStateController.isOccluded()) { reset(dozing /* hideBouncerWhenShowing */); } @@ -1081,7 +1080,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return whether a back press can be handled right now. */ public boolean canHandleBackPressed() { - return mBouncer.isShowing(); + return bouncerIsShowing(); } /** @@ -1094,7 +1093,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mCentralSurfaces.endAffordanceLaunch(); // The second condition is for SIM card locked bouncer - if (bouncerIsScrimmed() && needsFullscreenBouncer()) { + if (bouncerIsScrimmed() && !needsFullscreenBouncer()) { hideBouncer(false); updateStates(); } else { @@ -1124,8 +1123,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean isFullscreenBouncer() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.isFullScreenBouncer(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().isFullScreenBouncer(); } return mBouncer != null && mBouncer.isFullscreenBouncer(); } @@ -1284,15 +1283,15 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean shouldDismissOnMenuPressed() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.shouldDismissOnMenuPressed(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().shouldDismissOnMenuPressed(); } return mBouncer != null && mBouncer.shouldDismissOnMenuPressed(); } public boolean interceptMediaKey(KeyEvent event) { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.interceptMediaKey(event); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().interceptMediaKey(event); } return mBouncer != null && mBouncer.interceptMediaKey(event); } @@ -1301,8 +1300,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return true if the pre IME back event should be handled */ public boolean dispatchBackKeyEventPreIme() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.dispatchBackKeyEventPreIme(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().dispatchBackKeyEventPreIme(); } return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt index 0d52f46e571f..e498ae451400 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone.userswitcher import android.content.Intent import android.os.UserHandle import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter @@ -75,7 +76,7 @@ class StatusBarUserSwitcherControllerImpl @Inject constructor( null /* ActivityLaunchAnimator.Controller */, true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) } else { - userSwitcherDialogController.showDialog(view) + userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 9b8b6434827e..06cd12dd1a0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -24,29 +24,19 @@ import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @SysUISingleton class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) { - /** - * Returns true if we should run the new pipeline backend. - * - * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs, - * and logs the output state. - */ - fun isNewPipelineBackendEnabled(): Boolean = - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND) + /** True if we should display the mobile icons using the new status bar data pipeline. */ + fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS) - /** - * Returns true if we should run the new pipeline frontend *and* backend. - * - * The new pipeline frontend will use the outputted state from the new backend and will make the - * correct changes to the UI. - */ - fun isNewPipelineFrontendEnabled(): Boolean = - isNewPipelineBackendEnabled() && - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND) + /** True if we should display the wifi icon using the new status bar data pipeline. */ + fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON) + + // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the + // logging without getting the UI effects. /** - * Returns true if we should apply some coloring to icons that were rendered with the new + * Returns true if we should apply some coloring to the wifi icon that was rendered with the new * pipeline to help with debugging. */ - // For now, just always apply the debug coloring if we've enabled frontend rendering. - fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled() + // For now, just always apply the debug coloring if we've enabled the new icon. + fun useWifiDebugColoring(): Boolean = useNewWifiIcon() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt new file mode 100644 index 000000000000..7aa5ee1389f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings.Global +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.SettingObserver +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.util.settings.GlobalSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Provides data related to airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is + * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel] + * determine what parts of the wifi icon view should be shown. + * + * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo. + */ +interface AirplaneModeRepository { + /** Observable for whether the device is currently in airplane mode. */ + val isAirplaneMode: StateFlow<Boolean> +} + +@SysUISingleton +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class AirplaneModeRepositoryImpl +@Inject +constructor( + @Background private val bgHandler: Handler, + private val globalSettings: GlobalSettings, + logger: ConnectivityPipelineLogger, + @Application scope: CoroutineScope, +) : AirplaneModeRepository { + // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it. + override val isAirplaneMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val observer = + object : + SettingObserver( + globalSettings, + bgHandler, + Global.AIRPLANE_MODE_ON, + UserHandle.USER_ALL + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) { + trySend(value == 1) + } + } + + observer.isListening = true + trySend(observer.value == 1) + awaitClose { observer.isListening = false } + } + .distinctUntilChanged() + .logInputChange(logger, "isAirplaneMode") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + // When the observer starts listening, the flow will emit the current value so the + // initialValue here is irrelevant. + initialValue = false, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt new file mode 100644 index 000000000000..3e9b2c2ae809 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * The business logic layer for airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [AirplaneModeRepository] for more details. + */ +@SysUISingleton +class AirplaneModeInteractor +@Inject +constructor( + airplaneModeRepository: AirplaneModeRepository, + connectivityRepository: ConnectivityRepository, +) { + /** True if the device is currently in airplane mode. */ + val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode + + /** True if we're configured to force-hide the airplane mode icon and false otherwise. */ + val isForceHidden: Flow<Boolean> = + connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt new file mode 100644 index 000000000000..fe30c0169021 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Models the UI state for the status bar airplane mode icon. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for + * more details. + */ +@SysUISingleton +class AirplaneModeViewModel +@Inject +constructor( + interactor: AirplaneModeInteractor, + logger: ConnectivityPipelineLogger, + @Application private val scope: CoroutineScope, +) { + /** True if the airplane mode icon is currently visible in the status bar. */ + val isAirplaneModeIconVisible: StateFlow<Boolean> = + combine(interactor.isAirplaneMode, interactor.isForceHidden) { + isAirplaneMode, + isAirplaneIconForceHidden -> + isAirplaneMode && !isAirplaneIconForceHidden + } + .distinctUntilChanged() + .logOutputChange(logger, "isAirplaneModeIconVisible") + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 06d554232565..2aaa085645e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository @@ -30,6 +32,9 @@ import dagger.Module @Module abstract class StatusBarPipelineModule { @Binds + abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 681cf7254ae7..93448c1dee0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -39,7 +39,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import java.util.concurrent.Executor @@ -64,6 +63,9 @@ interface WifiRepository { /** Observable for the current wifi enabled status. */ val isWifiEnabled: StateFlow<Boolean> + /** Observable for the current wifi default status. */ + val isWifiDefault: StateFlow<Boolean> + /** Observable for the current wifi network. */ val wifiNetwork: StateFlow<WifiNetworkModel> @@ -103,7 +105,7 @@ class WifiRepositoryImpl @Inject constructor( merge(wifiNetworkChangeEvents, wifiStateChangeEvents) .mapLatest { wifiManager.isWifiEnabled } .distinctUntilChanged() - .logOutputChange(logger, "enabled") + .logInputChange(logger, "enabled") .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), @@ -111,6 +113,39 @@ class WifiRepositoryImpl @Inject constructor( ) } + override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow { + // Note: This callback doesn't do any logging because we already log every network change + // in the [wifiNetwork] callback. + val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + // This method will always be called immediately after the network becomes the + // default, in addition to any time the capabilities change while the network is + // the default. + // If this network contains valid wifi info, then wifi is the default network. + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + trySend(wifiInfo != null) + } + + override fun onLost(network: Network) { + // The system no longer has a default network, so wifi is definitely not default. + trySend(false) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .logInputChange(logger, "isWifiDefault") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow { var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 04b17ed2924a..3a3e611de96a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -59,6 +59,9 @@ class WifiInteractor @Inject constructor( /** Our current enabled status. */ val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled + /** Our current default status. */ + val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault + /** Our current wifi network. See [WifiNetworkModel]. */ val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 273be63eb8a2..25537b948517 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -91,6 +91,7 @@ object WifiViewBinder { val activityInView = view.requireViewById<ImageView>(R.id.wifi_in) val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out) val activityContainerView = view.requireViewById<View>(R.id.inout_container) + val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer) view.isVisible = true iconView.isVisible = true @@ -142,6 +143,12 @@ object WifiViewBinder { activityContainerView.isVisible = visible } } + + launch { + viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible -> + airplaneSpacer.isVisible = visible + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt index 40f948f9ee6c..95ab251422b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -32,6 +32,7 @@ class HomeWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -40,4 +41,5 @@ class HomeWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt index 9642ac42972e..86535d63f84f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -29,6 +29,7 @@ class KeyguardWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class KeyguardWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index e23f8c7e97e0..7cbdf5dbdf2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -44,11 +44,14 @@ abstract class LocationBasedWifiViewModel( /** True if the activity container view should be visible. */ val isActivityContainerVisible: Flow<Boolean>, + + /** True if the airplane spacer view should be visible. */ + val isAirplaneSpacerVisible: Flow<Boolean>, ) { /** The color that should be used to tint the icon. */ val tint: Flow<Int> = flowOf( - if (statusBarPipelineFlags.useNewPipelineDebugColoring()) { + if (statusBarPipelineFlags.useWifiDebugColoring()) { debugTint } else { DEFAULT_TINT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt index 0ddf90e21872..fd54c5f5062e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -29,6 +29,7 @@ class QsWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class QsWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index ebbd77b72014..89b96b7bc75d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange @@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.stateIn class WifiViewModel @Inject constructor( + airplaneModeViewModel: AirplaneModeViewModel, connectivityConstants: ConnectivityConstants, private val context: Context, logger: ConnectivityPipelineLogger, @@ -124,9 +126,10 @@ constructor( private val wifiIcon: StateFlow<Icon.Resource?> = combine( interactor.isEnabled, + interactor.isDefault, interactor.isForceHidden, interactor.wifiNetwork, - ) { isEnabled, isForceHidden, wifiNetwork -> + ) { isEnabled, isDefault, isForceHidden, wifiNetwork -> if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) { return@combine null } @@ -135,6 +138,7 @@ constructor( val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription()) return@combine when { + isDefault -> icon wifiConstants.alwaysShowIconIfEnabled -> icon !connectivityConstants.hasDataCapabilities -> icon wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon @@ -175,6 +179,12 @@ constructor( } .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the + // airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel + // that appropriately knows about both icons and sets the padding appropriately. + private val isAirplaneSpacerVisible: Flow<Boolean> = + airplaneModeViewModel.isAirplaneModeIconVisible + /** A view model for the status bar on the home screen. */ val home: HomeWifiViewModel = HomeWifiViewModel( @@ -183,6 +193,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar on keyguard. */ @@ -193,6 +204,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar in quick settings. */ @@ -203,6 +215,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index dc73d1f007c6..f63d65246d9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -36,6 +36,7 @@ import com.android.keyguard.KeyguardVisibilityHelper; import com.android.keyguard.dagger.KeyguardUserSwitcherScope; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -190,7 +191,8 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mUiEventLogger.log( LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP); - mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground); + mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(), + Expandable.fromView(mUserAvatarViewWithBackground)); }); mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() { diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 1a25e4df3a61..1a8aafb1a5f2 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -18,7 +18,6 @@ package com.android.systemui.temporarydisplay.chipbar import android.content.Context import android.graphics.Rect -import android.media.MediaRoute2Info import android.os.PowerManager import android.view.Gravity import android.view.MotionEvent @@ -27,25 +26,25 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.TransferStatus import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController -import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -78,11 +77,11 @@ open class ChipbarCoordinator @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger, private val falsingManager: FalsingManager, private val falsingCollector: FalsingCollector, private val viewUtil: ViewUtil, -) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>( + private val vibratorHelper: VibratorHelper, +) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>( context, logger, windowManager, @@ -104,15 +103,13 @@ open class ChipbarCoordinator @Inject constructor( override fun start() {} override fun updateView( - newInfo: ChipSenderInfo, + newInfo: ChipbarInfo, currentView: ViewGroup ) { // TODO(b/245610654): Adding logging here. - val chipState = newInfo.state - // Detect falsing touches on the chip. - parent = currentView.requireViewById(R.id.media_ttt_sender_chip) + parent = currentView.requireViewById(R.id.chipbar_root_view) parent.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) @@ -120,47 +117,54 @@ open class ChipbarCoordinator @Inject constructor( } } - // App icon - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( - context, newInfo.routeInfo.clientPackageName, logger - ) - val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon) - iconView.setImageDrawable(iconInfo.drawable) - iconView.contentDescription = iconInfo.contentDescription + // ---- Start icon ---- + val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon) + IconViewBinder.bind(newInfo.startIcon, iconView) - // Text - val otherDeviceName = newInfo.routeInfo.name.toString() - val chipText = chipState.getChipTextString(context, otherDeviceName) - currentView.requireViewById<TextView>(R.id.text).text = chipText + // ---- Text ---- + val textView = currentView.requireViewById<TextView>(R.id.text) + TextViewBinder.bind(textView, newInfo.text) + // ---- End item ---- // Loading currentView.requireViewById<View>(R.id.loading).visibility = - (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue() - - // Undo - val undoView = currentView.requireViewById<View>(R.id.undo) - val undoClickListener = chipState.undoClickListener( - this, - newInfo.routeInfo, - newInfo.undoCallback, - uiEventLogger, - falsingManager, - ) - undoView.setOnClickListener(undoClickListener) - undoView.visibility = (undoClickListener != null).visibleIfTrue() + (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() + + // Error + currentView.requireViewById<View>(R.id.error).visibility = + (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue() + + // Button + val buttonView = currentView.requireViewById<TextView>(R.id.end_button) + if (newInfo.endItem is ChipbarEndItem.Button) { + TextViewBinder.bind(buttonView, newInfo.endItem.text) + + val onClickListener = View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } - // Failure - currentView.requireViewById<View>(R.id.failure_icon).visibility = - (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue() + buttonView.setOnClickListener(onClickListener) + buttonView.visibility = View.VISIBLE + } else { + buttonView.visibility = View.GONE + } - // For accessibility + // ---- Overall accessibility ---- currentView.requireViewById<ViewGroup>( - R.id.media_ttt_sender_chip_inner - ).contentDescription = "${iconInfo.contentDescription} $chipText" + R.id.chipbar_inner + ).contentDescription = + "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " + + "${newInfo.text.loadText(context)}" + + // ---- Haptics ---- + newInfo.vibrationEffect?.let { + vibratorHelper.vibrate(it) + } } override fun animateViewIn(view: ViewGroup) { - val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner) + val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner) ViewHierarchyAnimator.animateAddition( chipInnerView, ViewHierarchyAnimator.Hotspot.TOP, @@ -175,7 +179,7 @@ open class ChipbarCoordinator @Inject constructor( override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { ViewHierarchyAnimator.animateRemoval( - view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner), + view.requireViewById<ViewGroup>(R.id.chipbar_inner), ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_ACCELERATE, ANIMATION_DURATION, @@ -197,13 +201,5 @@ open class ChipbarCoordinator @Inject constructor( } } -data class ChipSenderInfo( - val state: ChipStateSender, - val routeInfo: MediaRoute2Info, - val undoCallback: IUndoMediaTransferCallback? = null -) : TemporaryViewInfo { - override fun getTimeoutMs() = state.timeout -} - const val SENDER_TAG = "MediaTapToTransferSender" private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt new file mode 100644 index 000000000000..57fde87114d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.os.VibrationEffect +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.temporarydisplay.TemporaryViewInfo + +/** + * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. + * + * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales; + * on the right in RTL locales). + * @property text the text to display. + * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR + * locales; on the left in RTL locales). + * @property vibrationEffect an optional vibration effect when the chipbar is displayed + */ +data class ChipbarInfo( + val startIcon: Icon, + val text: Text, + val endItem: ChipbarEndItem?, + val vibrationEffect: VibrationEffect? = null, +) : TemporaryViewInfo + +/** The possible items to display at the end of the chipbar. */ +sealed class ChipbarEndItem { + /** A loading icon should be displayed. */ + object Loading : ChipbarEndItem() + + /** An error icon should be displayed. */ + object Error : ChipbarEndItem() + + /** + * A button with the provided [text] and [onClickListener] functionality should be displayed. + */ + data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem() + + // TODO(b/245610654): Add support for a generic icon. +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3d56f2317660..3ecb15b9d79c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -79,6 +79,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -114,6 +115,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final SecureSettings mSecureSettings; private final Executor mMainExecutor; private final Handler mBgHandler; + private final boolean mIsMonochromaticEnabled; private final Context mContext; private final boolean mIsMonetEnabled; private final UserTracker mUserTracker; @@ -363,6 +365,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) { mContext = context; + mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES); mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; @@ -665,8 +668,13 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { // Allow-list of Style objects that can be created from a setting string, i.e. can be // used as a system-wide theme. // - Content intentionally excluded, intended for media player, not system-wide - List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, - Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); + List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, + Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT)); + + if (mIsMonochromaticEnabled) { + validStyles.add(Style.MONOCHROMATIC); + } + Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt index ee785b62bd50..088cd93bdf7e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt @@ -36,9 +36,7 @@ class UserSwitcherPopupMenu( private var adapter: ListAdapter? = null init { - setBackgroundDrawable( - res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme()) - ) + setBackgroundDrawable(null) setModal(false) setOverlapAnchor(true) } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index d768b6dc195a..b16dc5403a57 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -220,7 +220,12 @@ constructor( val result = withContext(backgroundDispatcher) { manager.aliveUsers } if (result != null) { - _userInfos.value = result.sortedBy { it.creationTime } + _userInfos.value = + result + // Users should be sorted by ascending creation time. + .sortedBy { it.creationTime } + // The guest user is always last, regardless of creation time. + .sortedBy { it.isGuest } } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 0d5c64b83e6e..dda78aad54c6 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -429,6 +429,7 @@ constructor( isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) ) return @@ -443,6 +444,7 @@ constructor( isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) ) return @@ -477,6 +479,7 @@ constructor( userHandle = currentUser.userHandle, isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + dialogShower = dialogShower, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 08d7c5a26a25..177356e6b573 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -18,14 +18,18 @@ package com.android.systemui.user.domain.model import android.os.UserHandle +import com.android.systemui.qs.user.UserSwitchDialogController /** Encapsulates a request to show a dialog. */ -sealed class ShowDialogRequestModel { +sealed class ShowDialogRequestModel( + open val dialogShower: UserSwitchDialogController.DialogShower? = null, +) { data class ShowAddUserDialog( val userHandle: UserHandle, val isKeyguardShowing: Boolean, val showEphemeralMessage: Boolean, - ) : ShowDialogRequestModel() + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) data class ShowUserCreationDialog( val isGuest: Boolean, @@ -37,5 +41,6 @@ sealed class ShowDialogRequestModel { val isGuestEphemeral: Boolean, val isKeyguardShowing: Boolean, val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, - ) : ShowDialogRequestModel() + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index 938417f9dbe3..968af59e6c45 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -18,12 +18,15 @@ package com.android.systemui.user.ui.binder import android.content.Context +import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible @@ -36,6 +39,7 @@ import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView +import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children @@ -168,15 +172,10 @@ object UserSwitcherViewBinder { onDismissed: () -> Unit, ): UserSwitcherPopupMenu { return UserSwitcherPopupMenu(context).apply { + this.setDropDownGravity(Gravity.END) this.anchorView = anchorView setAdapter(adapter) setOnDismissListener { onDismissed() } - setOnItemClickListener { _, _, position, _ -> - val itemPositionExcludingHeader = position - 1 - adapter.getItem(itemPositionExcludingHeader).onClicked() - dismiss() - } - show() } } @@ -186,38 +185,67 @@ object UserSwitcherViewBinder { private val layoutInflater: LayoutInflater, ) : BaseAdapter() { - private val items = mutableListOf<UserActionViewModel>() + private var sections = listOf<List<UserActionViewModel>>() override fun getCount(): Int { - return items.size + return sections.size } - override fun getItem(position: Int): UserActionViewModel { - return items[position] + override fun getItem(position: Int): List<UserActionViewModel> { + return sections[position] } override fun getItemId(position: Int): Long { - return getItem(position).viewKey + return position.toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = - convertView - ?: layoutInflater.inflate( + val section = getItem(position) + val context = parent.context + val sectionView = + convertView as? LinearLayout + ?: LinearLayout(context, null).apply { + this.orientation = LinearLayout.VERTICAL + this.background = + parent.resources.getDrawable( + R.drawable.bouncer_user_switcher_popup_bg, + context.theme + ) + this.showDividers = SHOW_DIVIDER_MIDDLE + this.dividerDrawable = + context.getDrawable( + R.drawable.fullscreen_userswitcher_menu_item_divider + ) + } + sectionView.removeAllViewsInLayout() + + for (viewModel in section) { + val view = + layoutInflater.inflate( R.layout.user_switcher_fullscreen_popup_item, - parent, - false + /* parent= */ null ) - val viewModel = getItem(position) - view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId) - view.requireViewById<TextView>(R.id.text).text = - view.resources.getString(viewModel.textResourceId) - return view + view + .requireViewById<ImageView>(R.id.icon) + .setImageResource(viewModel.iconResourceId) + view.requireViewById<TextView>(R.id.text).text = + view.resources.getString(viewModel.textResourceId) + view.setOnClickListener { viewModel.onClicked() } + sectionView.addView(view) + } + return sectionView } fun setItems(items: List<UserActionViewModel>) { - this.items.clear() - this.items.addAll(items) + val primarySection = + items.filter { + it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + val secondarySection = + items.filter { + it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + this.sections = listOf(primarySection, secondarySection) notifyDataSetChanged() } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index f7e19c0ca810..e9217209530b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -19,8 +19,10 @@ package com.android.systemui.user.ui.dialog import android.app.Dialog import android.content.Context +import com.android.internal.jank.InteractionJankMonitor import com.android.settingslib.users.UserCreatingDialog import com.android.systemui.CoreStartable +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton @@ -71,37 +73,58 @@ constructor( } } - currentDialog = + val (dialog, dialogCuj) = when (request) { is ShowDialogRequestModel.ShowAddUserDialog -> - AddUserDialog( - context = context.get(), - userHandle = request.userHandle, - isKeyguardShowing = request.isKeyguardShowing, - showEphemeralMessage = request.showEphemeralMessage, - falsingManager = falsingManager.get(), - broadcastSender = broadcastSender.get(), - dialogLaunchAnimator = dialogLaunchAnimator.get(), + Pair( + AddUserDialog( + context = context.get(), + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager.get(), + broadcastSender = broadcastSender.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_ADD_NEW_USER_TAG, + ), ) is ShowDialogRequestModel.ShowUserCreationDialog -> - UserCreatingDialog( - context.get(), - request.isGuest, + Pair( + UserCreatingDialog( + context.get(), + request.isGuest, + ), + null, ) is ShowDialogRequestModel.ShowExitGuestDialog -> - ExitGuestDialog( - context = context.get(), - guestUserId = request.guestUserId, - isGuestEphemeral = request.isGuestEphemeral, - targetUserId = request.targetUserId, - isKeyguardShowing = request.isKeyguardShowing, - falsingManager = falsingManager.get(), - dialogLaunchAnimator = dialogLaunchAnimator.get(), - onExitGuestUserListener = request.onExitGuestUser, + Pair( + ExitGuestDialog( + context = context.get(), + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + onExitGuestUserListener = request.onExitGuestUser, + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_EXIT_GUEST_MODE_TAG, + ), ) } + currentDialog = dialog + + if (request.dialogShower != null && dialogCuj != null) { + request.dialogShower?.showDialog(dialog, dialogCuj) + } else { + dialog.show() + } - currentDialog?.show() interactor.get().onDialogShown() } } @@ -120,4 +143,9 @@ constructor( } } } + + companion object { + private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user" + private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode" + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java index ecb365f43e3f..2c317dd391c0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java @@ -172,10 +172,14 @@ public abstract class Condition implements CallbackController<Condition.Callback return Boolean.TRUE.equals(mIsConditionMet); } - private boolean shouldLog() { + protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } + protected final String getTag() { + return mTag; + } + /** * Callback that receives updates about whether the condition has been fulfilled. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java index 4824f6744c6e..cb430ba454f0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java @@ -117,6 +117,7 @@ public class Monitor { final SubscriptionState state = new SubscriptionState(subscription); mExecutor.execute(() -> { + if (shouldLog()) Log.d(mTag, "adding subscription"); mSubscriptions.put(token, state); // Add and associate conditions. @@ -143,7 +144,7 @@ public class Monitor { */ public void removeSubscription(@NotNull Subscription.Token token) { mExecutor.execute(() -> { - if (shouldLog()) Log.d(mTag, "removing callback"); + if (shouldLog()) Log.d(mTag, "removing subscription"); if (!mSubscriptions.containsKey(token)) { Log.e(mTag, "subscription not present:" + token); return; diff --git a/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto new file mode 100644 index 000000000000..b7166d96d401 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.systemui.util; + +option java_multiple_files = true; + +message ComponentNameProto { + string package_name = 1; + string class_name = 2; +} diff --git a/packages/SystemUI/tests/res/layout/custom_view_dark.xml b/packages/SystemUI/tests/res/layout/custom_view_dark.xml index 9e460a5819a9..112d73d2d7f2 100644 --- a/packages/SystemUI/tests/res/layout/custom_view_dark.xml +++ b/packages/SystemUI/tests/res/layout/custom_view_dark.xml @@ -14,6 +14,7 @@ limitations under the License. --> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/custom_view_dark_image" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ff000000" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt index 9d6aff219148..7b9b39f23c29 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt @@ -66,4 +66,13 @@ class BouncerKeyguardMessageAreaTest : SysuiTestCase() { underTest.setMessage(null) assertThat(underTest.text).isEqualTo("") } + + @Test + fun testSetNullClearsPreviousMessage() { + underTest.setMessage("something not null") + assertThat(underTest.text).isEqualTo("something not null") + + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 8a2c35410586..03efd06d0e5e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -17,17 +17,21 @@ package com.android.keyguard import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner +import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -37,6 +41,9 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import java.util.TimeZone import java.util.concurrent.Executor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -57,7 +64,7 @@ import org.mockito.junit.MockitoJUnit class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -72,8 +79,11 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockController: ClockFaceController @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents + @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository + private lateinit var repository: FakeKeyguardRepository - private lateinit var clockEventController: ClockEventController + private lateinit var underTest: ClockEventController @Before fun setUp() { @@ -86,8 +96,11 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(clock.events).thenReturn(events) whenever(clock.animations).thenReturn(animations) - clockEventController = ClockEventController( - statusBarStateController, + repository = FakeKeyguardRepository() + + underTest = ClockEventController( + KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -98,31 +111,33 @@ class ClockEventControllerTest : SysuiTestCase() { bgExecutor, featureFlags ) + underTest.clock = clock + + runBlocking(IMMEDIATE) { + underTest.registerListeners(parentView) + + repository.setDozing(true) + repository.setDozeAmount(1f) + } } @Test fun clockSet_validateInitialization() { - clockEventController.clock = clock - verify(clock).initialize(any(), anyFloat(), anyFloat()) } @Test fun clockUnset_validateState() { - clockEventController.clock = clock - clockEventController.clock = null + underTest.clock = null - assertEquals(clockEventController.clock, null) + assertEquals(underTest.clock, null) } @Test - fun themeChanged_verifyClockPaletteUpdated() { - clockEventController.clock = clock + fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() @@ -131,13 +146,10 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun fontChanged_verifyFontSizeUpdated() { - clockEventController.clock = clock + fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() @@ -146,10 +158,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -161,26 +170,21 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - - verify(animations, times(1)).charge() - } + fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, times(1)).charge() + } @Test - fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -192,25 +196,20 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, false) - - verify(animations, never()).charge() - } + fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, false) + + verify(animations, never()).charge() + } @Test - fun localeCallback_verifyClockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<BroadcastReceiver>() verify(broadcastDispatcher).registerReceiver( capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) @@ -221,10 +220,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_visibilityChanged_clockDozeCalled() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) @@ -236,10 +232,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timeFormat_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") @@ -248,11 +241,8 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timezoneChanged_clockNotified() { + fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock<TimeZone>() - clockEventController.clock = clock - clockEventController.registerListeners() - val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) @@ -261,10 +251,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_userSwitched_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) @@ -273,25 +260,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() { - clockEventController.clock = clock - clockEventController.registerListeners() + fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { + val job = underTest.listenForDozeAmount(this) + repository.setDozeAmount(0.4f) - val captor = argumentCaptor<StatusBarStateController.StateListener>() - verify(statusBarStateController).addCallback(capture(captor)) - captor.value.onDozeAmountChanged(0.4f, 0.6f) + yield() verify(animations).doze(0.4f) + + job.cancel() } @Test - fun unregisterListeners_validate() { - clockEventController.clock = clock - clockEventController.unregisterListeners() + fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { + underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) - verify(statusBarStateController).removeCallback(any()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 9b2bba612106..627d738a895f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -280,6 +280,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(); + verify(mClockEventController, times).registerListeners(mView); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 48e82397e826..b885d546c517 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -146,6 +146,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; + @Captor + private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor; private Configuration mConfiguration; @@ -475,6 +477,64 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt()); } + @Test + public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor, never()) + .requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock).showMessage(null, null); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null); + } + + private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { + mKeyguardSecurityContainerController.onViewAttached(); + verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture()); + return mSwipeListenerArgumentCaptor.getValue(); + } + private void setupConditionsToEnableSideFpsHint() { attachView(); setSideFpsHintEnabledFromResources(true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 7281bc8c851b..c6233b54c028 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -26,6 +26,7 @@ import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.google.common.truth.Truth.assertThat; @@ -648,6 +649,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT); } + @Test + public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException { + // This satisfies all the preconditions to run face auth. + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + bouncerFullyVisibleAndNotGoingToSleep(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isTrue(); + } + + @Test + public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException { + // This ensures face auth won't run. + biometricsDisabledForCurrentUser(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isFalse(); + } + private void testStrongAuthExceptOnBouncer(int strongAuth) { when(mKeyguardBypassController.canBypass()).thenReturn(true); mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java new file mode 100644 index 000000000000..6391a2c8eff7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.PointF; +import android.testing.AndroidTestingRunner; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MenuAnimationController}. */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class MenuAnimationControllerTest extends SysuiTestCase { + private MenuView mMenuView; + private MenuAnimationController mMenuAnimationController; + + @Before + public void setUp() throws Exception { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + mMenuAnimationController = new MenuAnimationController(mMenuView); + } + + @Test + public void moveToPosition_matchPosition() { + final PointF destination = new PointF(50, 60); + + mMenuAnimationController.moveToPosition(destination); + + assertThat(mMenuView.getTranslationX()).isEqualTo(50); + assertThat(mMenuView.getTranslationY()).isEqualTo(60); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java index d8b10e04705e..e62a3295a7e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.verify; import android.testing.AndroidTestingRunner; @@ -25,6 +26,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,13 +44,24 @@ public class MenuInfoRepositoryTest extends SysuiTestCase { @Mock private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged; + private MenuInfoRepository mMenuInfoRepository; + + @Before + public void setUp() { + mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged); + } + @Test public void menuSizeTypeChanged_verifyOnSizeTypeChanged() { - final MenuInfoRepository menuInfoRepository = - new MenuInfoRepository(mContext, mMockSettingsContentsChanged); - - menuInfoRepository.mMenuSizeContentObserver.onChange(true); + mMenuInfoRepository.mMenuSizeContentObserver.onChange(true); verify(mMockSettingsContentsChanged).onSizeTypeChanged(anyInt()); } + + @Test + public void menuOpacityChanged_verifyOnFadeEffectChanged() { + mMenuInfoRepository.mMenuFadeOutContentObserver.onChange(true); + + verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java new file mode 100644 index 000000000000..bf6d574a0f67 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS; +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link MenuItemAccessibilityDelegate}. */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private RecyclerView mStubListView; + private MenuView mMenuView; + private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; + private MenuAnimationController mMenuAnimationController; + private final Rect mDraggableBounds = new Rect(100, 200, 300, 400); + + @Before + public void setUp() { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + + final int halfScreenHeight = + stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2; + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView.setTranslationY(halfScreenHeight); + + doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds(); + mStubListView = new RecyclerView(mContext); + mMenuAnimationController = spy(new MenuAnimationController(mMenuView)); + mMenuItemAccessibilityDelegate = + new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate( + mStubListView), mMenuAnimationController); + } + + @Test + public void getAccessibilityActionList_matchSize() { + final AccessibilityNodeInfoCompat info = + new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); + + mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); + + assertThat(info.getActionList().size()).isEqualTo(5); + } + + @Test + public void performMoveTopLeftAction_matchPosition() { + final boolean moveTopLeftAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_top_left, + null); + + assertThat(moveTopLeftAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top); + } + + @Test + public void performMoveTopRightAction_matchPosition() { + final boolean moveTopRightAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_top_right, null); + + assertThat(moveTopRightAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top); + } + + @Test + public void performMoveBottomLeftAction_matchPosition() { + final boolean moveBottomLeftAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_bottom_left, null); + + assertThat(moveBottomLeftAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom); + } + + @Test + public void performMoveBottomRightAction_matchPosition() { + final boolean moveBottomRightAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_bottom_right, null); + + assertThat(moveBottomRightAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom); + } + + @Test + public void performMoveToEdgeAndHideAction_success() { + final boolean moveToEdgeAndHideAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_to_edge_and_hide, null); + + assertThat(moveToEdgeAndHideAction).isTrue(); + verify(mMenuAnimationController).moveToEdgeAndHide(); + } + + @Test + public void performMoveOutFromEdgeAction_success() { + final boolean moveOutEdgeAndShowAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_out_edge_and_show, null); + + assertThat(moveOutEdgeAndShowAction).isTrue(); + verify(mMenuAnimationController).moveOutEdgeAndShow(); + } + + @Test + public void performFocusAction_fadeIn() { + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + ACTION_ACCESSIBILITY_FOCUS, null); + + verify(mMenuAnimationController).fadeInNowIfEnabled(); + } + + @Test + public void performClearFocusAction_fadeOut() { + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); + + verify(mMenuAnimationController).fadeOutIfEnabled(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java new file mode 100644 index 000000000000..c5b9a294fc34 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static android.view.View.OVER_SCROLL_NEVER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.MotionEvent; +import android.view.WindowManager; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.filters.SmallTest; + +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.MotionEventHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Tests for {@link MenuListViewTouchHandler}. */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@SmallTest +public class MenuListViewTouchHandlerTest extends SysuiTestCase { + private final List<AccessibilityTarget> mStubTargets = new ArrayList<>( + Collections.singletonList(mock(AccessibilityTarget.class))); + private final MotionEventHelper mMotionEventHelper = new MotionEventHelper(); + private MenuView mStubMenuView; + private MenuListViewTouchHandler mTouchHandler; + private MenuAnimationController mMenuAnimationController; + private RecyclerView mStubListView; + + @Before + public void setUp() throws Exception { + final WindowManager windowManager = mContext.getSystemService(WindowManager.class); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + windowManager); + mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + mStubMenuView.setTranslationX(0); + mStubMenuView.setTranslationY(0); + mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView)); + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController); + final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); + mStubListView = (RecyclerView) mStubMenuView.getChildAt(0); + mStubListView.setAdapter(stubAdapter); + } + + @Test + public void onActionDownEvent_shouldCancelAnimations() { + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + + verify(mMenuAnimationController).cancelAnimations(); + } + + @Test + public void onActionMoveEvent_shouldMoveToPosition() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + mStubListView.setOverScrollMode(OVER_SCROLL_NEVER); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + assertThat(mStubMenuView.getTranslationX()).isEqualTo(offset); + assertThat(mStubMenuView.getTranslationY()).isEqualTo(offset); + } + + @Test + public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + final MotionEvent stubUpEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5, + MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent); + + verify(mMenuAnimationController).flingMenuThenSpringToEdge(anyFloat(), anyFloat(), + anyFloat()); + } + + @Test + public void dragMenuOutOfBoundsAndDrop_moveToLeftEdge_shouldMoveToEdgeAndHide() { + final int offset = -100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + final MotionEvent stubUpEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5, + MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent); + + verify(mMenuAnimationController).moveToEdgeAndHide(); + } + + @After + public void tearDown() { + mMotionEventHelper.recycleEvents(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index f782a446c627..8c8d6aca7cd7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java @@ -16,13 +16,24 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.view.WindowInsets.Type.displayCutout; +import static android.view.WindowInsets.Type.systemBars; + import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.Context; +import android.graphics.Insets; +import android.graphics.Rect; import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.test.filters.SmallTest; @@ -38,6 +49,7 @@ import org.mockito.junit.MockitoRule; /** Tests for {@link MenuViewLayerController}. */ @RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class MenuViewLayerControllerTest extends SysuiTestCase { @Rule @@ -46,10 +58,20 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { @Mock private WindowManager mWindowManager; + @Mock + private WindowMetrics mWindowMetrics; + private MenuViewLayerController mMenuViewLayerController; @Before public void setUp() throws Exception { + final WindowManager wm = mContext.getSystemService(WindowManager.class); + doAnswer(invocation -> wm.getMaximumWindowMetrics()).when( + mWindowManager).getMaximumWindowMetrics(); + mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340)); + when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets()); mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager); } @@ -68,4 +90,14 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { verify(mWindowManager).removeView(any(View.class)); } + + private WindowInsets stubDisplayInsets() { + final int stubStatusBarHeight = 118; + final int stubNavigationBarHeight = 125; + return new WindowInsets.Builder() + .setVisible(systemBars() | displayCutout(), true) + .setInsets(systemBars() | displayCutout(), + Insets.of(0, stubStatusBarHeight, 0, stubNavigationBarHeight)) + .build(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 8883cb783438..23c6ef1338b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertThat; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; +import android.view.WindowManager; import androidx.test.filters.SmallTest; @@ -44,7 +45,8 @@ public class MenuViewLayerTest extends SysuiTestCase { @Before public void setUp() throws Exception { - mMenuViewLayer = new MenuViewLayer(mContext); + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index 513044d2c20d..742ee53e99b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -24,11 +24,15 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.UiModeManager; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.WindowManager; import androidx.test.filters.SmallTest; +import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import org.junit.After; @@ -45,6 +49,8 @@ public class MenuViewTest extends SysuiTestCase { private int mNightMode; private UiModeManager mUiModeManager; private MenuView mMenuView; + private String mLastPosition; + private MenuViewAppearance mStubMenuViewAppearance; @Before public void setUp() throws Exception { @@ -52,8 +58,11 @@ public class MenuViewTest extends SysuiTestCase { mNightMode = mUiModeManager.getNightMode(); mUiModeManager.setNightMode(MODE_NIGHT_YES); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); - final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext); - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance)); + mLastPosition = Prefs.getString(mContext, + Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); } @Test @@ -74,8 +83,58 @@ public class MenuViewTest extends SysuiTestCase { assertThat(areInsetsMatched).isTrue(); } + @Test + public void onDraggingStart_matchInsets() { + mMenuView.onDraggingStart(); + final InstantInsetLayerDrawable insetLayerDrawable = + (InstantInsetLayerDrawable) mMenuView.getBackground(); + + assertThat(insetLayerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM)).isEqualTo(0); + assertThat(insetLayerDrawable.getLayerInsetTop(INDEX_MENU_ITEM)).isEqualTo(0); + assertThat(insetLayerDrawable.getLayerInsetRight(INDEX_MENU_ITEM)).isEqualTo(0); + assertThat(insetLayerDrawable.getLayerInsetBottom(INDEX_MENU_ITEM)).isEqualTo(0); + } + + @Test + public void onAnimationend_updatePositionForSharedPreference() { + final float percentageX = 0.0f; + final float percentageY = 0.5f; + + mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY)); + final String positionString = Prefs.getString(mContext, + Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); + final Position position = Position.fromString(positionString); + + assertThat(position.getPercentageX()).isEqualTo(percentageX); + assertThat(position.getPercentageY()).isEqualTo(percentageY); + } + + @Test + public void onEdgeChangedIfNeeded_moveToLeftEdge_matchRadii() { + final Rect draggableBounds = mStubMenuViewAppearance.getMenuDraggableBounds(); + mMenuView.setTranslationX(draggableBounds.right); + + mMenuView.setTranslationX(draggableBounds.left); + mMenuView.onEdgeChangedIfNeeded(); + final float[] radii = getMenuViewGradient().getCornerRadii(); + + assertThat(radii[0]).isEqualTo(0.0f); + assertThat(radii[1]).isEqualTo(0.0f); + assertThat(radii[6]).isEqualTo(0.0f); + assertThat(radii[7]).isEqualTo(0.0f); + } + + private InstantInsetLayerDrawable getMenuViewInsetLayer() { + return (InstantInsetLayerDrawable) mMenuView.getBackground(); + } + + private GradientDrawable getMenuViewGradient() { + return (GradientDrawable) getMenuViewInsetLayer().getDrawable(INDEX_MENU_ITEM); + } + @After public void tearDown() throws Exception { mUiModeManager.setNightMode(mNightMode); + Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, mLastPosition); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index cd50144bf2e8..d489656559c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_ import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater @@ -124,14 +125,18 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { whenever(udfpsEnrollView.context).thenReturn(context) } - private fun withReason(@ShowReason reason: Int, block: () -> Unit) { + private fun withReason( + @ShowReason reason: Int, + isDebuggable: Boolean = false, + block: () -> Unit + ) { controllerOverlay = UdfpsControllerOverlay( context, fingerprintManager, inflater, windowManager, accessibilityManager, statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, configurationController, systemClock, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, - controllerCallback, onTouch, activityLaunchAnimator + controllerCallback, onTouch, activityLaunchAnimator, isDebuggable ) block() } @@ -151,11 +156,29 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { } @Test + fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { showUdfpsOverlay(isEnrollUseCase = true) } @Test + fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() } private fun withRotation(@Rotation rotation: Int, block: () -> Unit) { @@ -373,21 +396,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_0 // touch at 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) } fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -395,21 +430,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_90 // touch at 0 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 90 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 180 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 270 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) } fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -417,21 +464,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_270 // touch at 0 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 90 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 180 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 270 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index eff47bd2ee98..49c6fd14997e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -665,7 +665,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN it is cancelled - mUdfpsController.onCancelUdfps(); + mUdfpsController.cancelAodInterrupt(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 3e9cf1e51b63..fa9c41a3cbb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -35,6 +35,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; @@ -71,6 +72,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; + @Mock private BatteryController mBatteryController; private final DockManagerFake mDockManager = new DockManagerFake(); private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -85,7 +88,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager, mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor, - mStatusBarStateController, mKeyguardStateController, mBatteryController, + mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager, + mBatteryController, mDockManager, mFakeExecutor, mFakeSystemClock); } @@ -137,9 +141,9 @@ public class FalsingCollectorImplTest extends SysuiTestCase { public void testUnregisterSensor_QS() { mFalsingCollector.onScreenTurningOn(); reset(mProximitySensor); - mFalsingCollector.setQsExpanded(true); + mFalsingCollector.onQsExpansionChanged(true); verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class)); - mFalsingCollector.setQsExpanded(false); + mFalsingCollector.onQsExpansionChanged(false); verify(mProximitySensor).register(any(ThresholdSensor.Listener.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index d96ca91e36bd..677c7bdcffe1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -40,6 +40,7 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.screenshot.TimeoutHandler; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -47,6 +48,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +@Ignore("b/254635291") @SmallTest @RunWith(AndroidJUnit4.class) public class ClipboardOverlayControllerTest extends SysuiTestCase { diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index b33f9a7f3933..2f206adc5acf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -425,7 +425,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testGesturesAllInitiallyRespectSettings() { - DozeSensors dozeSensors = new DozeSensors(getContext(), mSensorManager, mDozeParameters, + DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); @@ -437,7 +437,7 @@ public class DozeSensorsTest extends SysuiTestCase { private class TestableDozeSensors extends DozeSensors { TestableDozeSensors() { - super(getContext(), mSensorManager, mDozeParameters, + super(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 781dc1550048..6091d3a93f14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -23,10 +23,10 @@ import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -88,6 +88,8 @@ public class DozeTriggersTest extends SysuiTestCase { @Mock private ProximityCheck mProximityCheck; @Mock + private DozeLog mDozeLog; + @Mock private AuthController mAuthController; @Mock private UiEventLogger mUiEventLogger; @@ -127,7 +129,7 @@ public class DozeTriggersTest extends SysuiTestCase { mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters, asyncSensorManager, wakeLock, mDockManager, mProximitySensor, - mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(), + mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(), mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController, mDevicePostureController); mTriggers.setDozeMachine(mMachine); @@ -342,6 +344,16 @@ public class DozeTriggersTest extends SysuiTestCase { verify(mProximityCheck).destroy(); } + @Test + public void testIsExecutingTransition_dropPulse() { + when(mHost.isPulsePending()).thenReturn(false); + when(mMachine.isExecutingTransition()).thenReturn(true); + + mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null); + + verify(mDozeLog).tracePulseDropped(anyString(), eq(null)); + } + private void waitForSensorManager() { mExecutor.runAllReady(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index 65b44a14d2ad..65ae90b8f7e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -19,11 +19,17 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest import com.android.systemui.CoreStartable import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat +import java.io.FileDescriptor +import java.io.PrintWriter +import java.io.StringWriter +import javax.inject.Provider import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -31,9 +37,6 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.io.PrintWriter -import java.io.StringWriter -import javax.inject.Provider @SmallTest class DumpHandlerTest : SysuiTestCase() { @@ -47,6 +50,8 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var pw: PrintWriter + @Mock + private lateinit var fd: FileDescriptor @Mock private lateinit var dumpable1: Dumpable @@ -56,6 +61,11 @@ class DumpHandlerTest : SysuiTestCase() { private lateinit var dumpable3: Dumpable @Mock + private lateinit var protoDumpable1: ProtoDumpable + @Mock + private lateinit var protoDumpable2: ProtoDumpable + + @Mock private lateinit var buffer1: LogBuffer @Mock private lateinit var buffer2: LogBuffer @@ -88,7 +98,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN some of them are dumped explicitly val args = arrayOf("dumpable1", "dumpable3", "buffer2") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN only the requested ones have their dump() method called verify(dumpable1).dump(pw, args) @@ -107,7 +117,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN that module is dumped val args = arrayOf("dumpable1") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN its dump() method is called verify(dumpable1).dump(pw, args) @@ -124,7 +134,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a critical dump is requested val args = arrayOf("--dump-priority", "CRITICAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all modules are dumped (but no buffers) verify(dumpable1).dump(pw, args) @@ -145,7 +155,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a normal dump is requested val args = arrayOf("--dump-priority", "NORMAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all buffers are dumped (but no modules) verify(dumpable1, never()).dump( @@ -168,11 +178,35 @@ class DumpHandlerTest : SysuiTestCase() { val spw = PrintWriter(stringWriter) // When a config dump is requested - dumpHandler.dump(spw, arrayOf("config")) + dumpHandler.dump(fd, spw, arrayOf("config")) assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName) } + @Test + fun testDumpAllProtoDumpables() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO) + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2).dumpProto(any(), eq(args)) + } + + @Test + fun testDumpSingleProtoDumpable() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO, "protoDumpable1") + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2, never()).dumpProto(any(), any()) + } + private class EmptyCoreStartable : CoreStartable { override fun start() {} } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt index 4c6113870737..9628ee93ceff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt @@ -51,14 +51,6 @@ class FlagCommandTest : SysuiTestCase() { } @Test - fun noOpCommand() { - cmd.execute(pw, ArrayList()) - Mockito.verify(pw, Mockito.atLeastOnce()).println() - Mockito.verify(featureFlags).isEnabled(flagA) - Mockito.verify(featureFlags).isEnabled(flagB) - } - - @Test fun readFlagCommand() { cmd.execute(pw, listOf(flagA.id.toString())) Mockito.verify(featureFlags).isEnabled(flagA) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 000000000000..1b34100b1cef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.animation.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List<BigDecimal> { + val steps = mutableListOf<BigDecimal>() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { + // + 2 accounts for start and finish of automated transition + assertThat(steps.size).isEqualTo(fractions.size + 2) + + assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + fractions.forEachIndexed { index, fraction -> + assertThat(steps[index + 1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair<ValueAnimator, TestFrameCallbackProvider> { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index 7c83cb74bb77..6a4c0f60466d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -22,6 +22,9 @@ import android.graphics.drawable.Drawable import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -62,6 +65,34 @@ class MediaTttUtilsTest : SysuiTestCase() { } @Test + fun getIconFromPackageName_nullPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_invalidPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_validPackageName_returnsAppInfo() { + val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger) + + assertThat(icon) + .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME))) + } + + @Test fun getIconInfoFromPackageName_nullPackageName_returnsDefault() { val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 110bbb80df3a..fdeb3f5eb857 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -17,14 +17,19 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.PowerManager +import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -32,16 +37,18 @@ import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -60,20 +67,29 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class MediaTttSenderCoordinatorTest : SysuiTestCase() { + + // Note: This tests are a bit like integration tests because they use a real instance of + // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on + // the inputs from [MediaTttSenderCoordinator]. + private lateinit var underTest: MediaTttSenderCoordinator @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var vibratorHelper: VibratorHelper private lateinit var chipbarCoordinator: ChipbarCoordinator private lateinit var commandQueueCallback: CommandQueue.Callbacks + private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake @@ -85,6 +101,18 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) + whenever( + packageManager.getApplicationInfo( + eq(PACKAGE_NAME), + any<PackageManager.ApplicationInfoFlags>() + ) + ) + .thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) + fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -100,10 +128,10 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, + vibratorHelper, ) chipbarCoordinator.start() @@ -149,10 +177,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -163,10 +198,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -177,12 +219,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -193,12 +240,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -209,12 +261,66 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) } @Test @@ -225,12 +331,68 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) } @Test @@ -241,12 +403,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -257,12 +424,17 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) } @Test @@ -407,53 +579,113 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager).removeView(any()) } - private fun getChipView(): ViewGroup { + @Test + fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } + private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon) + private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) + private fun ChipStateSender.getExpectedStateText(): String? { + return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context) + } } +private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") - .setClientPackageName("com.android.systemui") + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 0badd861787d..1bc4719c70b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -147,6 +147,18 @@ public class ColorSchemeTest extends SysuiTestCase { } @Test + public void testMonochromatic() { + int colorInt = 0xffB3588A; // H350 C50 T50 + ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, + Style.MONOCHROMATIC /* style */); + int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + Assert.assertTrue( + Color.red(neutralMid) == Color.green(neutralMid) + && Color.green(neutralMid) == Color.blue(neutralMid) + ); + } + + @Test @SuppressWarnings("ResultOfMethodCallIgnored") public void testToString() { new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index 1c686c66e31e..5e9c1aaad309 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -22,7 +22,6 @@ import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,6 +51,7 @@ import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.TextView; import com.android.systemui.R; @@ -97,6 +97,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline; private ViewGroup mRootView; + private ViewGroup mSecurityFooterView; private TextView mFooterText; private TestableImageView mPrimaryFooterIcon; private QSSecurityFooter mFooter; @@ -121,21 +122,26 @@ public class QSSecurityFooterTest extends SysuiTestCase { Looper looper = mTestableLooper.getLooper(); Handler mainHandler = new Handler(looper); when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class)); - mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext) + mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) .build().inflate(R.layout.quick_settings_security_footer, null, false); mFooterUtils = new QSSecurityFooterUtils(getContext(), getContext().getSystemService(DevicePolicyManager.class), mUserTracker, mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator); - mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper, - mBroadcastDispatcher, mFooterUtils); - mFooterText = mRootView.findViewById(R.id.footer_text); - mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon); + mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController, + looper, mBroadcastDispatcher, mFooterUtils); + mFooterText = mSecurityFooterView.findViewById(R.id.footer_text); + mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon); when(mSecurityController.getDeviceOwnerComponentOnAnyUser()) .thenReturn(DEVICE_OWNER_COMPONENT); when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_DEFAULT); + + // mSecurityFooterView must have a ViewGroup parent so that + // DialogLaunchAnimator.Controller.fromView() does not return null. + mRootView = new FrameLayout(mContext); + mRootView.addView(mSecurityFooterView); ViewUtils.attachView(mRootView); mFooter.init(); @@ -153,7 +159,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -165,7 +171,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertEquals(mContext.getString(R.string.quick_settings_disclosure_management), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -181,7 +187,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -200,7 +206,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString( R.string.quick_settings_financed_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -217,7 +223,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -227,8 +233,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -241,8 +247,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertTrue(mRootView.isClickable()); - assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertTrue(mSecurityFooterView.isClickable()); + assertEquals(View.VISIBLE, + mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -254,8 +261,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -734,11 +741,11 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testDialogUsesDialogLauncher() { when(mSecurityController.isDeviceManaged()).thenReturn(true); - mFooter.onClick(mRootView); + mFooter.onClick(mSecurityFooterView); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any()); + verify(mDialogLaunchAnimator).show(any(), any()); } @Test @@ -775,7 +782,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any()); + verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any()); AlertDialog dialog = dialogCaptor.getValue(); dialog.create(); @@ -817,8 +824,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(), any()); - // Pretend view is not visible temporarily - mRootView.onVisibilityAggregated(false); + // Pretend view is not attached anymore. + mRootView.removeView(mSecurityFooterView); captor.getValue().onReceive(mContext, new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)); mTestableLooper.processAllMessages(); 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 3c58b6fc1354..c452872a527e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -52,6 +52,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -114,8 +115,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private DumpManager mDumpManager; @Mock - private QSTile.State mMockState; - @Mock private CentralSurfaces mCentralSurfaces; @Mock private QSLogger mQSLogger; @@ -195,7 +194,6 @@ public class QSTileHostTest extends SysuiTestCase { } private void setUpTileFactory() { - when(mMockState.toString()).thenReturn(MOCK_STATE_STRING); // Only create this kind of tiles when(mDefaultFactory.createTile(anyString())).thenAnswer( invocation -> { @@ -209,7 +207,11 @@ public class QSTileHostTest extends SysuiTestCase { } else if ("na".equals(spec)) { return new NotAvailableTile(mQSTileHost); } else if (CUSTOM_TILE_SPEC.equals(spec)) { - return mCustomTile; + QSTile tile = mCustomTile; + QSTile.State s = mock(QSTile.State.class); + s.spec = spec; + when(mCustomTile.getState()).thenReturn(s); + return tile; } else if ("internet".equals(spec) || "wifi".equals(spec) || "cell".equals(spec)) { @@ -647,7 +649,7 @@ public class QSTileHostTest extends SysuiTestCase { @Test public void testSetTileRemoved_removedBySystem() { int user = mUserTracker.getUserId(); - saveSetting("spec1" + CUSTOM_TILE_SPEC); + saveSetting("spec1," + CUSTOM_TILE_SPEC); // This will be done by TileServiceManager mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); @@ -658,6 +660,27 @@ public class QSTileHostTest extends SysuiTestCase { .getBoolean(CUSTOM_TILE.flattenToString(), false)); } + @Test + public void testProtoDump_noTiles() { + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(0, proto.tiles.length); + } + + @Test + public void testTilesInOrder() { + saveSetting("spec1," + CUSTOM_TILE_SPEC); + + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(2, proto.tiles.length); + assertEquals("spec1", proto.tiles[0].getSpec()); + assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName); + assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className); + } + private SharedPreferences getSharedPreferenecesForUser(int user) { return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user); } @@ -707,12 +730,9 @@ public class QSTileHostTest extends SysuiTestCase { @Override public State newTileState() { - return mMockState; - } - - @Override - public State getState() { - return mMockState; + State s = mock(QSTile.State.class); + when(s.toString()).thenReturn(MOCK_STATE_STRING); + return s; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt new file mode 100644 index 000000000000..629c663943db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt @@ -0,0 +1,104 @@ +package com.android.systemui.qs + +import android.content.ComponentName +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TileStateToProtoTest : SysuiTestCase() { + + companion object { + private const val TEST_LABEL = "label" + private const val TEST_SUBTITLE = "subtitle" + private const val TEST_SPEC = "spec" + private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls") + } + + @Test + fun platformTile_INACTIVE() { + val state = + QSTile.State().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_INACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun componentTile_UNAVAILABLE() { + val state = + QSTile.State().apply { + spec = CustomTile.toSpec(TEST_COMPONENT) + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_UNAVAILABLE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isFalse() + assertThat(proto?.hasComponentName()).isTrue() + val componentName = proto?.componentName + assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName) + assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className) + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun booleanState_ACTIVE() { + val state = + QSTile.BooleanState().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + value = true + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE) + assertThat(proto?.hasBooleanState()).isTrue() + assertThat(proto?.booleanState).isTrue() + } + + @Test + fun noSpec_returnsNull() { + val state = + QSTile.State().apply { + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 3c258077c29d..2c2ddbb9b8c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -23,13 +23,13 @@ import android.os.UserHandle import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.internal.logging.nano.MetricsProto import com.android.internal.logging.testing.FakeMetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite @@ -70,13 +70,13 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils) val quickSettingsContext = mock<Context>() - underTest.showDeviceMonitoringDialog(quickSettingsContext) - verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) - val view = mock<View>() - whenever(view.context).thenReturn(quickSettingsContext) - underTest.showDeviceMonitoringDialog(view) + underTest.showDeviceMonitoringDialog(quickSettingsContext, null) verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + + val expandable = mock<Expandable>() + underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable) } @Test @@ -85,8 +85,8 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger) val globalActionsDialogLite = mock<GlobalActionsDialogLite>() - val view = mock<View>() - underTest.showPowerMenuDialog(globalActionsDialogLite, view) + val expandable = mock<Expandable>() + underTest.showPowerMenuDialog(globalActionsDialogLite, expandable) // Event is logged. val logs = uiEventLogger.logs @@ -99,7 +99,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { .showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -167,11 +167,11 @@ class FooterActionsInteractorTest : SysuiTestCase() { userSwitchDialogController = userSwitchDialogController, ) - val view = mock<View>() - underTest.showUserSwitcher(view) + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. - verify(userSwitchDialogController).showDialog(view) + verify(userSwitchDialogController).showDialog(context, expandable) } @Test @@ -184,12 +184,9 @@ class FooterActionsInteractorTest : SysuiTestCase() { activityStarter = activityStarter, ) - // The clicked view. The context is necessary because it's used to build the intent, that - // we check below. - val view = mock<View>() - whenever(view.context).thenReturn(context) - - underTest.showUserSwitcher(view) + // The clicked expandable. + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. val intentCaptor = argumentCaptor<Intent>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 9d908fdfb976..0a34810f4d3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -20,12 +20,12 @@ import android.content.DialogInterface import android.content.Intent import android.provider.Settings import android.testing.AndroidTestingRunner -import android.view.View import android.widget.Button import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +64,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock private lateinit var userDetailViewAdapter: UserDetailView.Adapter @Mock - private lateinit var launchView: View + private lateinit var launchExpandable: Expandable @Mock private lateinit var neutralButton: Button @Mock @@ -79,7 +80,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(launchView.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext) controller = UserSwitchDialogController( @@ -94,32 +94,34 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Test fun showDialog_callsDialogShow() { - controller.showDialog(launchView) - verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean()) + val launchController = mock<DialogLaunchAnimator.Controller>() + `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController) + controller.showDialog(context, launchExpandable) + verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean()) verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) } @Test fun dialog_showForAllUsers() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setShowForAllUsers(true) } @Test fun dialog_cancelOnTouchOutside() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setCanceledOnTouchOutside(true) } @Test fun adapterAndGridLinked() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>()) } @Test fun doneButtonLogsCorrectly() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor)) @@ -132,7 +134,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_noFalsing_opensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) @@ -153,7 +155,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_Falsing_notOpensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index d095add1c660..e444a3909408 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -18,6 +18,7 @@ package com.android.systemui.shade; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; @@ -108,6 +109,7 @@ import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.media.MediaDataManager; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; @@ -254,6 +256,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private PrivacyDotViewController mPrivacyDotViewController; @Mock private NavigationModeController mNavigationModeController; + @Mock private NavigationBarController mNavigationBarController; @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; @Mock private ContentResolver mContentResolver; @Mock private TapAgainViewController mTapAgainViewController; @@ -377,6 +380,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( + mDumpManager, mock(HeadsUpManagerPhone.class), new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager, mInteractionJankMonitor), @@ -392,6 +396,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mConfigurationController, mStatusBarStateController, mFalsingManager, + mShadeExpansionStateManager, mLockscreenShadeTransitionController, new FalsingCollectorFake(), mDumpManager); @@ -430,6 +435,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mView.getParent()).thenReturn(mViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); + when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); mMainHandler = new Handler(Looper.getMainLooper()); NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter = @@ -473,6 +479,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mPrivacyDotViewController, mTapAgainViewController, mNavigationModeController, + mNavigationBarController, mFragmentService, mContentResolver, mRecordingController, @@ -757,6 +764,38 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + // Start shade collapse with swipe up + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + + // simulate touch that does not exceed touch slop + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + mNotificationPanelViewController.setTouchSlopExceeded(false); + + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + // fling should still be called after a touch that does not exceed touch slop + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + } + + @Test public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(false); @@ -1568,7 +1607,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mNotificationPanelViewController.mStatusBarStateListener; statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); - when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false); // This sets the dozing state that is read when onMiddleClicked is eventually invoked. mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); @@ -1583,7 +1622,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mNotificationPanelViewController.mStatusBarStateListener; statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); - when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true); // This sets the dozing state that is read when onMiddleClicked is eventually invoked. mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt index 12ef036d89d0..bdafc7df33bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt @@ -66,6 +66,8 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { @Mock private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController @Mock + private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock private lateinit var featureFlags: FeatureFlags @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> @@ -96,6 +98,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) @@ -380,6 +383,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index ad3d3d2958cb..95cf9d60b511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -88,6 +88,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private AuthController mAuthController; + @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; @@ -103,7 +104,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController) { + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) { @Override protected boolean isDebuggable() { return false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt index 44cbe51a30ac..fbb8ebfb3e3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager @@ -56,6 +57,7 @@ class PulseExpansionHandlerTest : SysuiTestCase() { private val configurationController: ConfigurationController = mock() private val statusBarStateController: StatusBarStateController = mock() private val falsingManager: FalsingManager = mock() + private val shadeExpansionStateManager: ShadeExpansionStateManager = mock() private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock() private val falsingCollector: FalsingCollector = mock() private val dumpManager: DumpManager = mock() @@ -65,7 +67,8 @@ class PulseExpansionHandlerTest : SysuiTestCase() { fun setUp() { whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight) - pulseExpansionHandler = PulseExpansionHandler( + pulseExpansionHandler = + PulseExpansionHandler( mContext, wakeUpCoordinator, bypassController, @@ -74,10 +77,11 @@ class PulseExpansionHandlerTest : SysuiTestCase() { configurationController, statusBarStateController, falsingManager, + shadeExpansionStateManager, lockscreenShadeTransitionController, falsingCollector, dumpManager - ) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index 4b458f5a9123..dda7fadde2d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -31,8 +31,8 @@ public class GroupEntryBuilder { private long mCreationTime = 0; @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY; private NotifSection mNotifSection; - private NotificationEntry mSummary = null; - private List<NotificationEntry> mChildren = new ArrayList<>(); + @Nullable private NotificationEntry mSummary = null; + private final List<NotificationEntry> mChildren = new ArrayList<>(); /** Builds a new instance of GroupEntry */ public GroupEntry build() { @@ -41,7 +41,9 @@ public class GroupEntryBuilder { ge.getAttachState().setSection(mNotifSection); ge.setSummary(mSummary); - mSummary.setParent(ge); + if (mSummary != null) { + mSummary.setParent(ge); + } for (NotificationEntry child : mChildren) { ge.addChild(child); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 851517e1e35b..3b05321e1a6b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1498,45 +1498,8 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test - public void testMissingRankingWhenRemovalFeatureIsDisabled() { + public void testMissingRanking() { // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false); - String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; - String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; - NotificationEntry entry1 = mCollectionListener.getEntry(key1); - NotificationEntry entry2 = mCollectionListener.getEntry(key2); - clearInvocations(mCollectionListener); - - // GIVEN the message for removing key1 gets does not reach NotifCollection - Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); - // WHEN the message for removing key2 arrives - mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); - - // THEN only entry2 gets removed - verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); - verify(mCollectionListener).onEntryCleanUp(eq(entry2)); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); - verify(mLogger, never()).logRecoveredRankings(any(), anyInt()); - clearInvocations(mCollectionListener, mLogger); - - // WHEN a ranking update includes key1 again - mNoMan.setRanking(key1, ranking1); - mNoMan.issueRankingUpdate(); - - // VERIFY that we do nothing but log the 'recovery' - verify(mCollectionListener).onRankingUpdate(any()); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); - verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0)); - } - - @Test - public void testMissingRankingWhenRemovalFeatureIsEnabled() { - // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true); String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; NotificationEntry entry1 = mCollectionListener.getEntry(key1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 340bc96f80c2..3ff7639e9262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -674,7 +674,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should + // and is new enough to do so assertFalse(mEntry.hasInterrupted()) + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -690,8 +692,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { - // GIVEN that mEntry has alerted in the past + // GIVEN that mEntry has alerted in the past, even if it's new mEntry.setInterruption() + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -725,6 +728,27 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mEntry) } + @Test + fun testOnRankingApplied_entryUpdatedButTooOld() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it was actually added 10s ago + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000) + + // WHEN it is updated to HUN and then a ranking update occurs + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index f4adf6927e31..b6b0b7738997 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -181,7 +181,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testInflatesNewNotification() { // WHEN there is a new notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); // THEN we inflate it @@ -194,7 +194,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testRebindsInflatedNotificationsOnUpdate() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -213,7 +213,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntrySmartReplyAdditionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -232,7 +232,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntryChangedToMinimizedSectionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertFalse(mParamsCaptor.getValue().isLowPriority()); @@ -254,7 +254,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { public void testMinimizedEntryMovedIntoGroupWillRebindViews() { // GIVEN an inflated, minimized notification setSectionIsLowPriority(true); - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertTrue(mParamsCaptor.getValue().isLowPriority()); @@ -275,7 +275,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntryRankChangeWillNotRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -294,7 +294,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testDoesntFilterInflatedNotifs() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -330,9 +330,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(entry); } - mCollectionListener.onEntryAdded(summary); + mCollectionListener.onEntryInit(summary); for (NotificationEntry entry : children) { - mCollectionListener.onEntryAdded(entry); + mCollectionListener.onEntryInit(entry); } mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry)); @@ -393,6 +393,70 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test + public void testNullGroupSummary() { + // GIVEN a newly-posted group with a summary and two children + final GroupEntry group = new GroupEntryBuilder() + .setCreationTime(400) + .setSummary(getNotificationEntryBuilder().setId(1).build()) + .addChild(getNotificationEntryBuilder().setId(2).build()) + .addChild(getNotificationEntryBuilder().setId(3).build()) + .build(); + fireAddEvents(List.of(group)); + final NotificationEntry child0 = group.getChildren().get(0); + final NotificationEntry child1 = group.getChildren().get(1); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // WHEN the summary is pruned + new GroupEntryBuilder() + .setCreationTime(400) + .addChild(child0) + .addChild(child1) + .build(); + + // WHEN all of the children (but not the summary) finish inflating + mNotifInflater.invokeInflateCallbackForEntry(child0); + mNotifInflater.invokeInflateCallbackForEntry(child1); + + // THEN the entire group is not filtered out + assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); + } + + @Test + public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() { + // GIVEN a newly-posted group with a summary and two children + final String groupKey = "test_reinflate_group"; + final int summaryId = 1; + final GroupEntry group = new GroupEntryBuilder() + .setKey(groupKey) + .setCreationTime(400) + .setSummary(getNotificationEntryBuilder().setId(summaryId).setImportance(1).build()) + .addChild(getNotificationEntryBuilder().setId(2).build()) + .addChild(getNotificationEntryBuilder().setId(3).build()) + .build(); + fireAddEvents(List.of(group)); + final NotificationEntry summary = group.getSummary(); + final NotificationEntry child0 = group.getChildren().get(0); + final NotificationEntry child1 = group.getChildren().get(1); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // WHEN all of the children (but not the summary) finish inflating + mNotifInflater.invokeInflateCallbackForEntry(child0); + mNotifInflater.invokeInflateCallbackForEntry(child1); + mNotifInflater.invokeInflateCallbackForEntry(summary); + + // WHEN the summary is updated and starts re-inflating + summary.setRanking(new RankingBuilder(summary.getRanking()).setImportance(4).build()); + fireUpdateEvents(summary); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // THEN the entire group is still not filtered out + assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); + } + + @Test public void testCompletedInflatedGroupsAreReleased() { // GIVEN a newly-posted group with a summary and two children final GroupEntry group = new GroupEntryBuilder() @@ -412,7 +476,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater.invokeInflateCallbackForEntry(child1); mNotifInflater.invokeInflateCallbackForEntry(summary); - // THEN the entire group is still filtered out + // THEN the entire group is no longer filtered out assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); @@ -494,7 +558,11 @@ public class PreparationCoordinatorTest extends SysuiTestCase { private void fireAddEvents(NotificationEntry entry) { mCollectionListener.onEntryInit(entry); - mCollectionListener.onEntryAdded(entry); + mCollectionListener.onEntryInit(entry); + } + + private void fireUpdateEvents(NotificationEntry entry) { + mCollectionListener.onEntryUpdated(entry); } private static final String TEST_MESSAGE = "TEST_MESSAGE"; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index 46f630b7db63..ea311da3e20b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -51,12 +51,14 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; +import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -97,6 +99,7 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { NotifPipelineFlags mFlags; @Mock KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + UiEventLoggerFake mUiEventLoggerFake; @Mock PendingIntent mPendingIntent; @@ -107,6 +110,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false); + mUiEventLoggerFake = new UiEventLoggerFake(); + mNotifInterruptionStateProvider = new NotificationInterruptStateProviderImpl( mContext.getContentResolver(), @@ -120,7 +125,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { mLogger, mMockHandler, mFlags, - mKeyguardNotificationVisibilityProvider); + mKeyguardNotificationVisibilityProvider, + mUiEventLoggerFake); mNotifInterruptionStateProvider.mUseHeadsUp = true; } @@ -442,6 +448,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } @Test @@ -600,6 +613,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt index 16e2441c556b..f69839b7087c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt @@ -28,30 +28,21 @@ import android.widget.RemoteViews import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.NotificationUtils -import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) -class NotificationMemoryMonitorTest : SysuiTestCase() { - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - } +class NotificationMemoryMeterTest : SysuiTestCase() { @Test fun currentNotificationMemoryUse_plainNotification() { val notification = createBasicNotification().build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -69,8 +60,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() { val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -92,8 +83,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { RemoteViews(context.packageName, android.R.layout.list_content) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -112,8 +103,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444) val notification = createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = 444444, @@ -141,8 +132,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { .bigLargeIcon(bigPictureIcon) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -167,8 +158,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { createBasicNotification() .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent)) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -203,8 +194,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { .addHistoricMessage(historicMessage) ) .build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -225,8 +216,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888) val extender = Notification.CarExtender().setLargeIcon(carIcon) val notification = createBasicNotification().extend(extender).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -246,8 +237,8 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888) val wearExtender = Notification.WearableExtender().setBackground(wearBackground) val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build() - val nmm = createNMMWithNotifications(listOf(notification)) - val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) assertNotificationObjectSizes( memoryUse = memoryUse, smallIcon = notification.smallIcon.bitmap.allocationByteCount, @@ -283,10 +274,10 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { extender: Int, style: String?, styleIcon: Int, - hasCustomView: Boolean + hasCustomView: Boolean, ) { assertThat(memoryUse.packageName).isEqualTo("test_pkg") - assertThat(memoryUse.notificationId) + assertThat(memoryUse.notificationKey) .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0")) assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) @@ -301,21 +292,14 @@ class NotificationMemoryMonitorTest : SysuiTestCase() { } private fun getUseObject( - singleItemUseList: List<NotificationMemoryUsage> + singleItemUseList: List<NotificationMemoryUsage>, ): NotificationMemoryUsage { assertThat(singleItemUseList).hasSize(1) return singleItemUseList[0] } - private fun createNMMWithNotifications( - notifications: List<Notification> - ): NotificationMemoryMonitor { - val notifPipeline: NotifPipeline = mock() - val notificationEntries = - notifications.map { n -> - NotificationEntryBuilder().setTag("test").setNotification(n).build() - } - whenever(notifPipeline.allNotifs).thenReturn(notificationEntries) - return NotificationMemoryMonitor(notifPipeline, mock()) - } + private fun createNotificationEntry( + notification: Notification, + ): NotificationEntry = + NotificationEntryBuilder().setTag("test").setNotification(notification).build() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt new file mode 100644 index 000000000000..3a16fb33388b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt @@ -0,0 +1,148 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.tests.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class NotificationMemoryViewWalkerTest : SysuiTestCase() { + + private lateinit var testHelper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + } + + @Test + fun testViewWalker_nullRow_returnsEmptyView() { + val result = NotificationMemoryViewWalker.getViewUsage(null) + assertThat(result).isNotNull() + assertThat(result).isEmpty() + } + + @Test + fun testViewWalker_plainNotification() { + val row = testHelper.createRow() + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_bigPictureNotification() { + val bigPicture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val largeIcon = Icon.createWithBitmap(Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888)) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setLargeIcon(largeIcon) + .setStyle(Notification.BigPictureStyle().bigPicture(bigPicture)) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + bigPicture.allocationByteCount, + 0, + bigPicture.allocationByteCount + + icon.bitmap.allocationByteCount + + largeIcon.bitmap.allocationByteCount + ) + ) + + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_customView() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + + val views = RemoteViews(mContext.packageName, R.layout.custom_view_dark) + views.setImageViewBitmap(R.id.custom_view_dark_image, bitmap) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setCustomContentView(views) + .setCustomBigContentView(views) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ad497a2ec1e1..6de8bd5f3670 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -80,6 +80,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.internal.statusbar.IStatusBarService; @@ -98,7 +99,8 @@ import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -271,7 +273,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private OngoingCallController mOngoingCallController; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private LockscreenShadeTransitionController mLockscreenTransitionController; - @Mock private FeatureFlags mFeatureFlags; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private WallpaperManager mWallpaperManager; @Mock private IWallpaperManager mIWallpaperManager; @@ -296,9 +297,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); - private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); - private InitController mInitController = new InitController(); + private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + private final InitController mInitController = new InitController(); private final DumpManager mDumpManager = new DumpManager(); @Before @@ -322,7 +324,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), new Handler(TestableLooper.get(this).getLooper()), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class)); + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class)); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class)); @@ -1017,6 +1020,60 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void collapseShade_callsAnimateCollapsePanels_whenExpanded() { + // GIVEN the shade is expanded + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() { + // GIVEN the shade is collapsed + mCentralSurfaces.setPanelExpanded(false); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is NOT called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() { when(mKeyguardStateController.isShowing()).thenReturn(false); setFoldedStates(FOLD_STATE_FOLDED); @@ -1102,7 +1159,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super( contentResolver, powerManager, @@ -1115,7 +1173,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { logger, mainHandler, flags, - keyguardNotificationVisibilityProvider + keyguardNotificationVisibilityProvider, + uiEventLogger ); mUseHeadsUp = true; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 8da8d049516e..0c35659b458a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -117,7 +117,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor; @Mock private BouncerInteractor mBouncerInteractor; @Mock private BouncerView mBouncerView; -// @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference; @Mock private BouncerViewDelegate mBouncerViewDelegate; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt index bf432388ad28..eba3b04f3472 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags @@ -34,8 +33,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -91,7 +90,7 @@ class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { fun testStartActivity() { `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(View::class.java)) + verify(userSwitcherDialogController).showDialog(any(), any()) `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) statusBarUserSwitcherContainer.callOnClick() verify(activityStarter).startActivity(any(Intent::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt new file mode 100644 index 000000000000..b7a6c0125cfa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings.Global +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class AirplaneModeRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeRepositoryImpl + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var bgHandler: Handler + private lateinit var scope: CoroutineScope + private lateinit var settings: FakeSettings + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + bgHandler = Handler(Looper.getMainLooper()) + scope = CoroutineScope(IMMEDIATE) + settings = FakeSettings() + settings.userId = UserHandle.USER_ALL + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun isAirplaneMode_initiallyGetsSettingsValue() = + runBlocking(IMMEDIATE) { + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + + val job = underTest.isAirplaneMode.launchIn(this) + + assertThat(underTest.isAirplaneMode.value).isTrue() + + job.cancel() + } + + @Test + fun isAirplaneMode_settingUpdated_valueUpdated() = + runBlocking(IMMEDIATE) { + val job = underTest.isAirplaneMode.launchIn(this) + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + yield() + assertThat(underTest.isAirplaneMode.value).isTrue() + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt new file mode 100644 index 000000000000..63bbdfca0071 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAirplaneModeRepository : AirplaneModeRepository { + private val _isAirplaneMode = MutableStateFlow(false) + override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode + + fun setIsAirplaneMode(isAirplaneMode: Boolean) { + _isAirplaneMode.value = isAirplaneMode + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt new file mode 100644 index 000000000000..33a80e1a3dd6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeInteractorTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeInteractor + + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + + @Before + fun setUp() { + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + } + + @Test + fun isAirplaneMode_matchesRepo() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + assertThat(latest).isFalse() + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoHasWifiHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt new file mode 100644 index 000000000000..76016a121e68 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeViewModelTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeViewModel + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var interactor: AirplaneModeInteractor + private lateinit var scope: CoroutineScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + scope = CoroutineScope(IMMEDIATE) + + underTest = + AirplaneModeViewModel( + interactor, + logger, + scope, + ) + } + + @Test + fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(false) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_forceHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index f751afc195b2..2f18ce31217e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -27,6 +27,9 @@ class FakeWifiRepository : WifiRepository { private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault + private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> = MutableStateFlow(WifiNetworkModel.Inactive) override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork @@ -38,6 +41,10 @@ class FakeWifiRepository : WifiRepository { _isWifiEnabled.value = enabled } + fun setIsWifiDefault(default: Boolean) { + _isWifiDefault.value = default + } + fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { _wifiNetwork.value = wifiNetworkModel } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt index 0ba0bd623c39..a64a4bd2e57a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt @@ -222,6 +222,83 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.ssid).thenReturn(SSID) + } + + getDefaultNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo) + ) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(mock()) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + // First, add a network + getDefaultNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat(underTest.isWifiDefault.value).isTrue() + + // WHEN the network is lost + getDefaultNetworkCallback().onLost(NETWORK) + + // THEN we update to false + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest @@ -745,6 +822,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { return callbackCaptor.value!! } + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun createWifiNetworkCapabilities( wifiInfo: WifiInfo, isValidated: Boolean = true, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index 39b886af1cb8..71b8bab87d19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -178,6 +178,29 @@ class WifiInteractorTest : SysuiTestCase() { } @Test + fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .isDefault + .onEach { latest = it } + .launchIn(this) + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + wifiRepository.setIsWifiDefault(false) + yield() + assertThat(latest).isFalse() + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) { val wifiNetwork = WifiNetworkModel.Active( networkId = 45, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 4efb13520ebf..c5841098010a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -63,11 +66,13 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor private lateinit var viewModel: WifiViewModel private lateinit var scope: CoroutineScope + private lateinit var airplaneModeViewModel: AirplaneModeViewModel @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() @@ -77,12 +82,22 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(Dispatchers.Unconfined) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) viewModel = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index a3ad028519bb..a1afcd71e3c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -22,11 +22,14 @@ import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -64,19 +67,31 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = + AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) } @After @@ -88,6 +103,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase fun wifiIcon() = runBlocking(IMMEDIATE) { wifiRepository.setIsWifiEnabled(testCase.enabled) + wifiRepository.setIsWifiDefault(testCase.isDefault) connectivityRepository.setForceHiddenIcons( if (testCase.forceHidden) { setOf(ConnectivitySlot.WIFI) @@ -101,6 +117,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase .thenReturn(testCase.hasDataCapabilities) underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, @@ -125,19 +142,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase } else { testCase.expected.contentDescription.invoke(context) } - assertThat(iconFlow.value?.contentDescription?.getAsString()) + assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) .isEqualTo(expectedContentDescription) job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } - } - internal data class Expected( /** The resource that should be used for the icon. */ @DrawableRes val iconResource: Int, @@ -159,6 +169,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase val forceHidden: Boolean = false, val alwaysShowIconWhenEnabled: Boolean = false, val hasDataCapabilities: Boolean = true, + val isDefault: Boolean = false, val network: WifiNetworkModel, /** The expected output. Null if we expect the output to be null. */ @@ -169,6 +180,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase "forceHidden=$forceHidden, " + "showWhenEnabled=$alwaysShowIconWhenEnabled, " + "hasDataCaps=$hasDataCapabilities, " + + "isDefault=$isDefault, " + "network=$network) then " + "EXPECTED($expected)" } @@ -303,6 +315,46 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase ), ), + // isDefault = true => all Inactive and Active networks shown + TestCase( + isDefault = true, + network = WifiNetworkModel.Inactive, + expected = + Expected( + iconResource = WIFI_NO_NETWORK, + contentDescription = { context -> + "${context.getString(WIFI_NO_CONNECTION)}," + + context.getString(NO_INTERNET) + }, + description = "No network icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3), + expected = + Expected( + iconResource = WIFI_NO_INTERNET_ICONS[3], + contentDescription = { context -> + "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," + + context.getString(NO_INTERNET) + }, + description = "No internet level 3 icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[1], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[1]) + }, + description = "Full internet level 1 icon", + ), + ), + // network = CarrierMerged => not shown TestCase( network = WifiNetworkModel.CarrierMerged, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 3169eef83f07..7d2c56098584 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -20,8 +20,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository @@ -55,19 +59,31 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) + createAndSetViewModel() } @@ -76,6 +92,8 @@ class WifiViewModelTest : SysuiTestCase() { scope.cancel() } + // See [WifiViewModelIconParameterizedTest] for additional view model tests. + // Note on testing: [WifiViewModel] exposes 3 different instances of // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact // same data for icon, activity, etc. flows. So, most of these tests will test just one of the @@ -460,11 +478,64 @@ class WifiViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + private fun createAndSetViewModel() { // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow // creations rely on certain config values that we mock out in individual tests. This method // allows tests to create the view model only after those configs are correctly set up. underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 6225d0c722ae..9fbf159ec348 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -16,11 +16,8 @@ package com.android.systemui.temporarydisplay.chipbar -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.media.MediaRoute2Info import android.os.PowerManager +import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -31,19 +28,19 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.sender.ChipStateSender -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -53,7 +50,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -64,437 +60,293 @@ import org.mockito.MockitoAnnotations class ChipbarCoordinatorTest : SysuiTestCase() { private lateinit var underTest: FakeChipbarCoordinator - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var applicationInfo: ApplicationInfo - @Mock - private lateinit var logger: MediaTttLogger - @Mock - private lateinit var accessibilityManager: AccessibilityManager - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var powerManager: PowerManager - @Mock - private lateinit var windowManager: WindowManager - @Mock - private lateinit var falsingManager: FalsingManager - @Mock - private lateinit var falsingCollector: FalsingCollector - @Mock - private lateinit var viewUtil: ViewUtil - private lateinit var fakeAppIconDrawable: Drawable + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var viewUtil: ViewUtil + @Mock private lateinit var vibratorHelper: VibratorHelper private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake - private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger @Before fun setUp() { MockitoAnnotations.initMocks(this) - - fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! - whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) - whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) - whenever(packageManager.getApplicationInfo( - eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>() - )).thenReturn(applicationInfo) - context.setMockPackageManager(packageManager) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) uiEventLoggerFake = UiEventLoggerFake() - senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) - - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) - underTest = FakeChipbarCoordinator( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - senderUiEventLogger, - falsingManager, - falsingCollector, - viewUtil, - ) + underTest = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + vibratorHelper, + ) underTest.start() } @Test - fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToStartCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToEndCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } + fun displayView_loadedIcon_correctlyRendered() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - @Test - fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToReceiverTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - @Test - fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToThisDeviceTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.drawable).isEqualTo(drawable) + assertThat(iconView.contentDescription).isEqualTo("loadedCD") } @Test - fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToReceiverSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + fun displayView_resourceIcon_correctlyRendered() { + val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToReceiverSucceeded(undoCallback = null)) - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.contentDescription) + .isEqualTo(contentDescription.loadContentDescription(context)) } @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } + fun displayView_loadedText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("display view text here"), + endItem = null, + ) + ) - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()).isEqualTo("display view text here") } @Test - fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() + fun displayView_resourceText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Resource(R.string.screenrecord_start_error), + endItem = null, + ) + ) - assertThat(undoCallbackCalled).isFalse() + assertThat(getChipbarView().getChipText()) + .isEqualTo(context.getString(R.string.screenrecord_start_error)) } @Test - fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() + fun displayView_endItemNull_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + ) + ) - assertThat(undoCallbackCalled).isTrue() + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id + fun displayView_endItemLoading_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) ) - } - @Test - fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToThisDeviceSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null)) + fun displayView_endItemError_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } + fun displayView_endItemButton_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + onClickListener = {}, + ), + ) + ) - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().text).isEqualTo("button text") + assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue() } @Test - fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() + fun displayView_endItemButtonClicked_falseTap_listenerNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) ) - } - @Test - fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToReceiverFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } + getChipbarView().getEndButton().performClick() - @Test - fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToThisDeviceFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(isClicked).isFalse() } @Test - fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() { - underTest.displayView(almostCloseToStartCast()) - underTest.displayView(transferToReceiverTriggered()) + fun displayView_endItemButtonClicked_notFalseTap_listenerRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - } + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) - @Test - fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverSucceeded()) + getChipbarView().getEndButton().performClick() - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE) + assertThat(isClicked).isTrue() } @Test - fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() { - underTest.displayView(transferToReceiverTriggered()) + fun displayView_vibrationEffect_doubleClickEffect() { underTest.displayView( - transferToReceiverSucceeded( - object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), ) ) - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE) + verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)) } @Test - fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() { - underTest.displayView(transferToReceiverSucceeded()) - underTest.displayView(almostCloseToStartCast()) + fun updateView_viewUpdated() { + // First, display a view + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE) - } + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + ) + ) - @Test - fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverFailed()) + val chipbarView = getChipbarView() + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD") + assertThat(chipbarView.getChipText()).isEqualTo("title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + + // WHEN the view is updated + val newDrawable = context.getDrawable(R.drawable.ic_cake)!! + underTest.updateView( + ChipbarInfo( + Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ), + chipbarView + ) - assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) + // THEN we display the new view + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD") + assertThat(chipbarView.getChipText()).isEqualTo("new title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } - private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - private fun ViewGroup.getLoadingIconVisibility(): Int = - this.requireViewById<View>(R.id.loading).visibility + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) + private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button) - private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - private fun getChipView(): ViewGroup { + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } - - // TODO(b/245610654): For now, the below methods are duplicated between this test and - // [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator], - // these will no longer be duplicated. - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) } -private const val APP_NAME = "Fake app name" -private const val OTHER_DEVICE_NAME = "My Tablet" -private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 - -private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) - .addFeature("feature") - .setClientPackageName(PACKAGE_NAME) - .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 10704ac8fc67..17d402319246 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -24,8 +24,8 @@ import android.view.accessibility.AccessibilityManager import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil @@ -39,10 +39,10 @@ class FakeChipbarCoordinator( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - uiEventLogger: MediaTttSenderUiEventLogger, falsingManager: FalsingManager, falsingCollector: FalsingCollector, viewUtil: ViewUtil, + vibratorHelper: VibratorHelper, ) : ChipbarCoordinator( context, @@ -52,10 +52,10 @@ class FakeChipbarCoordinator( accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, + vibratorHelper, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt index d951f366c595..525d8371c9ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -110,7 +110,7 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { val thirdExpectedValue = setUpUsers( count = 2, - hasGuest = true, + isLastGuestUser = true, selectedIndex = 1, ) underTest.refreshUsers() @@ -121,21 +121,25 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { } @Test - fun `refreshUsers - sorts by creation time`() = runSelfCancelingTest { + fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { underTest = create(this) val unsortedUsers = setUpUsers( count = 3, selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest ) - unsortedUsers[0].creationTime = 900 - unsortedUsers[1].creationTime = 700 - unsortedUsers[2].creationTime = 999 - val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2]) var userInfos: List<UserInfo>? = null - var selectedUserInfo: UserInfo? = null underTest.userInfos.onEach { userInfos = it }.launchIn(this) - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) underTest.refreshUsers() assertThat(userInfos).isEqualTo(expectedUsers) @@ -143,14 +147,14 @@ class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { private fun setUpUsers( count: Int, - hasGuest: Boolean = false, + isLastGuestUser: Boolean = false, selectedIndex: Int = 0, ): List<UserInfo> { val userInfos = (0 until count).map { index -> createUserInfo( index, - isGuest = hasGuest && index == count - 1, + isGuest = isLastGuestUser && index == count - 1, ) } whenever(manager.aliveUsers).thenReturn(userInfos) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt index e80d5166d088..f682e31c0547 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R.drawable.ic_account_circle import com.android.systemui.R import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.model.ShowDialogRequestModel @@ -317,14 +318,16 @@ class UserInteractorRefactoredTest : UserInteractorTest() { keyguardRepository.setKeyguardShowing(false) var dialogRequest: ShowDialogRequestModel? = null val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogShower: UserSwitchDialogController.DialogShower = mock() - underTest.executeAction(UserActionModel.ADD_USER) + underTest.executeAction(UserActionModel.ADD_USER, dialogShower) assertThat(dialogRequest) .isEqualTo( ShowDialogRequestModel.ShowAddUserDialog( userHandle = userInfos[0].userHandle, isKeyguardShowing = false, showEphemeralMessage = false, + dialogShower = dialogShower, ) ) 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 09da52e7685c..fa7ebf6a2449 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -80,6 +80,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; @@ -91,6 +92,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; @@ -190,6 +192,8 @@ public class BubblesTest extends SysuiTestCase { private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private AuthController mAuthController; + @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; @@ -290,7 +294,7 @@ public class BubblesTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController); + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager); mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); @@ -343,7 +347,8 @@ public class BubblesTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), mock(Handler.class), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class) + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class) ); when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor); mBubbleController = new TestableBubbleController( diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java index 9635faf6e858..e5316bc83a12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.PowerManager; import android.service.dreams.IDreamManager; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; @@ -46,7 +47,8 @@ public class TestableNotificationInterruptStateProviderImpl NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super(contentResolver, powerManager, dreamManager, @@ -58,7 +60,8 @@ public class TestableNotificationInterruptStateProviderImpl logger, mainHandler, flags, - keyguardNotificationVisibilityProvider); + keyguardNotificationVisibilityProvider, + uiEventLogger); mUseHeadsUp = true; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index 5d52be2675e3..a60b7735fbd4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -26,7 +26,7 @@ class FakeFeatureFlags : FeatureFlags { private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>() init { - Flags.getFlagFields().forEach { field -> + Flags.flagFields.forEach { field -> val flag: Flag<*> = field.get(null) as Flag<*> knownFlagNames[flag.id] = field.name } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 725b1f41372c..0c126805fb78 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.StatusBarState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow<StatusBarState> = _statusBarState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt index 527258579372..c33ce5d9484d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener import kotlinx.coroutines.flow.MutableStateFlow @@ -54,7 +54,7 @@ class FakeFgsManagerController( override fun init() {} - override fun showDialog(viewLaunchedFrom: View?) {} + override fun showDialog(expandable: Expandable?) {} override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { numRunningPackagesListeners.add(listener) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 2a9aeddc9aa8..325da4ead666 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -57,7 +57,6 @@ import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.time.FakeSystemClock import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -68,7 +67,6 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, - private val fakeClock: FakeSystemClock = FakeSystemClock(), ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index a185b585aeda..346fc6c4ab96 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -872,6 +872,33 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } @Override + public void setAppWidgetHidden(String callingPackage, int hostId) { + final int userId = UserHandle.getCallingUserId(); + + if (DEBUG) { + Slog.i(TAG, "setAppWidgetHidden() " + userId); + } + + mSecurityPolicy.enforceCallFromPackage(callingPackage); + + synchronized (mLock) { + ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false); + + HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage); + Host host = lookupHostLocked(id); + + if (host != null) { + try { + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + } catch (NullPointerException e) { + Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); + throw e; + } + } + } + } + + @Override public void deleteAppWidgetId(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING index 38d937288569..37c47baa813b 100644 --- a/services/companion/TEST_MAPPING +++ b/services/companion/TEST_MAPPING @@ -8,14 +8,6 @@ }, { "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases" - }, - { - "name": "CtsOsTestCases", - "options": [ - { - "include-filter": "android.os.cts.CompanionDeviceManagerTest" - } - ] } ] } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 0cf79153ce77..1150b83083cf 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3584,6 +3584,13 @@ class StorageManagerService extends IStorageManager.Stub final boolean includeSharedProfile = (flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0; + // When the caller is the app actually hosting external storage, we + // should never attempt to augment the actual storage volume state, + // otherwise we risk confusing it with race conditions as users go + // through various unlocked states + final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid, + mMediaStoreAuthorityAppId); + // Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile if (includeSharedProfile) { try { @@ -3596,8 +3603,13 @@ class StorageManagerService extends IStorageManager.Stub // Checking first entry in packagesFromUid is enough as using "sharedUserId" // mechanism is rare and discouraged. Also, Apps that share same UID share the same // permissions. - if (!mStorageManagerInternal.hasExternalStorageAccess(callingUid, - packagesFromUid[0])) { + // Allowing Media Provider is an exception, Media Provider process should be allowed + // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access. + // Note that ordinarily Media provider process has the above permission, but if they + // are revoked, Storage Volume(s) should still be returned. + if (!callerIsMediaStore + && !mStorageManagerInternal.hasExternalStorageAccess(callingUid, + packagesFromUid[0])) { throw new SecurityException("Only File Manager Apps permitted"); } } catch (RemoteException re) { @@ -3610,13 +3622,6 @@ class StorageManagerService extends IStorageManager.Stub // point final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM); - // When the caller is the app actually hosting external storage, we - // should never attempt to augment the actual storage volume state, - // otherwise we risk confusing it with race conditions as users go - // through various unlocked states - final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid, - mMediaStoreAuthorityAppId); - final boolean userIsDemo; final boolean userKeyUnlocked; final boolean storagePermission; diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index f7833b0f36fd..2652ebec5255 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -2581,33 +2581,39 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (!checkNotifyPermission("notifyBarringInfo()")) { return; } - if (barringInfo == null) { - log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId); - mBarringInfo.set(phoneId, new BarringInfo()); + if (!validatePhoneId(phoneId)) { + loge("Received invalid phoneId for BarringInfo = " + phoneId); return; } synchronized (mRecords) { - if (validatePhoneId(phoneId)) { - mBarringInfo.set(phoneId, barringInfo); - // Barring info is non-null - BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy(); - if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo); - for (Record r : mRecords) { - if (r.matchTelephonyCallbackEvent( - TelephonyCallback.EVENT_BARRING_INFO_CHANGED) - && idMatch(r, subId, phoneId)) { - try { - if (DBG_LOC) { - log("notifyBarringInfo: mBarringInfo=" - + barringInfo + " r=" + r); - } - r.callback.onBarringInfoChanged( - checkFineLocationAccess(r, Build.VERSION_CODES.BASE) - ? barringInfo : biNoLocation); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (barringInfo == null) { + loge("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId); + mBarringInfo.set(phoneId, new BarringInfo()); + return; + } + if (barringInfo.equals(mBarringInfo.get(phoneId))) { + if (VDBG) log("Ignoring duplicate barring info."); + return; + } + mBarringInfo.set(phoneId, barringInfo); + // Barring info is non-null + BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy(); + if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo); + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_BARRING_INFO_CHANGED) + && idMatch(r, subId, phoneId)) { + try { + if (DBG_LOC) { + log("notifyBarringInfo: mBarringInfo=" + + barringInfo + " r=" + r); } + r.callback.onBarringInfoChanged( + checkFineLocationAccess(r, Build.VERSION_CODES.BASE) + ? barringInfo : biNoLocation); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7a09109e377b..82af12e2f47f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -3967,8 +3967,12 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.w(TAG, msg); throw new SecurityException(msg); } + final boolean hasKillAllPermission = checkCallingPermission( + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) == PERMISSION_GRANTED; + final int callingUid = Binder.getCallingUid(); + final int callingAppId = UserHandle.getAppId(callingUid); - userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), + userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, true, ALLOW_FULL_ONLY, "killBackgroundProcesses", null); final int[] userIds = mUserController.expandUserId(userId); @@ -3983,7 +3987,7 @@ public class ActivityManagerService extends IActivityManager.Stub targetUserId)); } catch (RemoteException e) { } - if (appId == -1) { + if (appId == -1 || (!hasKillAllPermission && appId != callingAppId)) { Slog.w(TAG, "Invalid packageName: " + packageName); return; } @@ -4002,11 +4006,11 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void killAllBackgroundProcesses() { - if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) + if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) != PackageManager.PERMISSION_GRANTED) { final String msg = "Permission Denial: killAllBackgroundProcesses() from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES; + + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES; Slog.w(TAG, msg); throw new SecurityException(msg); } @@ -4042,11 +4046,11 @@ public class ActivityManagerService extends IActivityManager.Stub * processes, or {@code -1} to ignore the process state */ void killAllBackgroundProcessesExcept(int minTargetSdk, int maxProcState) { - if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) + if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) != PackageManager.PERMISSION_GRANTED) { final String msg = "Permission Denial: killAllBackgroundProcessesExcept() from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES; + + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES; Slog.w(TAG, msg); throw new SecurityException(msg); } @@ -13373,27 +13377,19 @@ public class ActivityManagerService extends IActivityManager.Stub int callingPid; boolean instantApp; synchronized(this) { - if (caller != null) { - callerApp = getRecordForAppLOSP(caller); - if (callerApp == null) { - throw new SecurityException( - "Unable to find app for caller " + caller - + " (pid=" + Binder.getCallingPid() - + ") when registering receiver " + receiver); - } - if (callerApp.info.uid != SYSTEM_UID - && !callerApp.getPkgList().containsKey(callerPackage) - && !"android".equals(callerPackage)) { - throw new SecurityException("Given caller package " + callerPackage - + " is not running in process " + callerApp); - } - callingUid = callerApp.info.uid; - callingPid = callerApp.getPid(); - } else { - callerPackage = null; - callingUid = Binder.getCallingUid(); - callingPid = Binder.getCallingPid(); + callerApp = getRecordForAppLOSP(caller); + if (callerApp == null) { + Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller); + return null; + } + if (callerApp.info.uid != SYSTEM_UID + && !callerApp.getPkgList().containsKey(callerPackage) + && !"android".equals(callerPackage)) { + throw new SecurityException("Given caller package " + callerPackage + + " is not running in process " + callerApp); } + callingUid = callerApp.info.uid; + callingPid = callerApp.getPid(); instantApp = isInstantApp(callerApp, callerPackage, callingUid); userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true, @@ -14700,13 +14696,14 @@ public class ActivityManagerService extends IActivityManager.Stub // Non-system callers can't declare that a broadcast is alarm-related. // The PendingIntent invocation case is handled in PendingIntentRecord. if (bOptions != null && callingUid != SYSTEM_UID) { - if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) { + if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) + || bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { if (DEBUG_BROADCAST) { Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm-related"); + + " may not flag broadcast as alarm or interactive"); } throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm-related"); + "Non-system callers may not flag broadcasts as alarm or interactive"); } } diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index a4a1c2f0d87c..28a81e6ee145 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -167,7 +167,7 @@ public class BroadcastConstants { */ public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS; private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis"; - private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER; + private static final long DEFAULT_DELAY_NORMAL_MILLIS = 1_000; /** * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts @@ -175,7 +175,7 @@ public class BroadcastConstants { */ public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS; private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis"; - private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER; + private static final long DEFAULT_DELAY_CACHED_MILLIS = 10_000; /** * For {@link BroadcastQueueModernImpl}: Maximum number of complete diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 0d6ac1d57387..868c3ae6da50 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -103,6 +103,13 @@ class BroadcastProcessQueue { private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>(); /** + * Ordered collection of "urgent" broadcasts that are waiting to be + * dispatched to this process, in the same representation as + * {@link #mPending}. + */ + private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(); + + /** * Broadcast actively being dispatched to this process. */ private @Nullable BroadcastRecord mActive; @@ -140,12 +147,16 @@ class BroadcastProcessQueue { private int mCountOrdered; private int mCountAlarm; private int mCountPrioritized; + private int mCountInteractive; + private int mCountResultTo; + private int mCountInstrumented; private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE; private @Reason int mRunnableAtReason = REASON_EMPTY; private boolean mRunnableAtInvalidated; private boolean mProcessCached; + private boolean mProcessInstrumented; private String mCachedToString; private String mCachedToShortString; @@ -172,40 +183,65 @@ class BroadcastProcessQueue { */ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { - // If caller wants to replace, walk backwards looking for any matches if (record.isReplacePending()) { - final Iterator<SomeArgs> it = mPending.descendingIterator(); - final Object receiver = record.receivers.get(recordIndex); - while (it.hasNext()) { - final SomeArgs args = it.next(); - final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; - final Object testReceiver = testRecord.receivers.get(args.argi1); - if ((record.callingUid == testRecord.callingUid) - && (record.userId == testRecord.userId) - && record.intent.filterEquals(testRecord.intent) - && isReceiverEquals(receiver, testReceiver)) { - // Exact match found; perform in-place swap - args.arg1 = record; - args.argi1 = recordIndex; - args.argi2 = blockedUntilTerminalCount; - onBroadcastDequeued(testRecord); - onBroadcastEnqueued(record); - return; - } + boolean didReplace = replaceBroadcastInQueue(mPending, + record, recordIndex, blockedUntilTerminalCount) + || replaceBroadcastInQueue(mPendingUrgent, + record, recordIndex, blockedUntilTerminalCount); + if (didReplace) { + return; } } // Caller isn't interested in replacing, or we didn't find any pending // item to replace above, so enqueue as a new broadcast - SomeArgs args = SomeArgs.obtain(); - args.arg1 = record; - args.argi1 = recordIndex; - args.argi2 = blockedUntilTerminalCount; - mPending.addLast(args); + SomeArgs newBroadcastArgs = SomeArgs.obtain(); + newBroadcastArgs.arg1 = record; + newBroadcastArgs.argi1 = recordIndex; + newBroadcastArgs.argi2 = blockedUntilTerminalCount; + + // Cross-broadcast prioritization policy: some broadcasts might warrant being + // issued ahead of others that are already pending, for example if this new + // broadcast is in a different delivery class or is tied to a direct user interaction + // with implicit responsiveness expectations. + final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending; + queue.addLast(newBroadcastArgs); onBroadcastEnqueued(record); } /** + * Searches from newest to oldest, and at the first matching pending broadcast + * it finds, replaces it in-place and returns -- does not attempt to handle + * "duplicate" broadcasts in the queue. + * <p> + * @return {@code true} if it found and replaced an existing record in the queue; + * {@code false} otherwise. + */ + private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, + @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { + final Iterator<SomeArgs> it = queue.descendingIterator(); + final Object receiver = record.receivers.get(recordIndex); + while (it.hasNext()) { + final SomeArgs args = it.next(); + final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; + final Object testReceiver = testRecord.receivers.get(args.argi1); + if ((record.callingUid == testRecord.callingUid) + && (record.userId == testRecord.userId) + && record.intent.filterEquals(testRecord.intent) + && isReceiverEquals(receiver, testReceiver)) { + // Exact match found; perform in-place swap + args.arg1 = record; + args.argi1 = recordIndex; + args.argi2 = blockedUntilTerminalCount; + onBroadcastDequeued(testRecord); + onBroadcastEnqueued(record); + return true; + } + } + return false; + } + + /** * Functional interface that tests a {@link BroadcastRecord} that has been * previously enqueued in {@link BroadcastProcessQueue}. */ @@ -233,8 +269,18 @@ class BroadcastProcessQueue { */ public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer, boolean andRemove) { + boolean didSomething = forEachMatchingBroadcastInQueue(mPending, + predicate, consumer, andRemove); + didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent, + predicate, consumer, andRemove); + return didSomething; + } + + private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, + @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer, + boolean andRemove) { boolean didSomething = false; - final Iterator<SomeArgs> it = mPending.iterator(); + final Iterator<SomeArgs> it = queue.iterator(); while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; @@ -255,6 +301,18 @@ class BroadcastProcessQueue { } /** + * Update the actively running "warm" process for this process. + */ + public void setProcess(@Nullable ProcessRecord app) { + this.app = app; + if (app != null) { + setProcessInstrumented(app.getActiveInstrumentation() != null); + } else { + setProcessInstrumented(false); + } + } + + /** * Update if this process is in the "cached" state, typically signaling that * broadcast dispatch should be paused or delayed. */ @@ -266,6 +324,18 @@ class BroadcastProcessQueue { } /** + * Update if this process is in the "instrumented" state, typically + * signaling that broadcast dispatch should bypass all pauses or delays, to + * avoid holding up test suites. + */ + public void setProcessInstrumented(boolean instrumented) { + if (mProcessInstrumented != instrumented) { + mProcessInstrumented = instrumented; + invalidateRunnableAt(); + } + } + + /** * Return if we know of an actively running "warm" process for this queue. */ public boolean isProcessWarm() { @@ -273,13 +343,12 @@ class BroadcastProcessQueue { } public int getPreferredSchedulingGroupLocked() { - if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) { - // We have an important broadcast somewhere down the queue, so + if (mCountForeground > 0) { + // We have a foreground broadcast somewhere down the queue, so // boost priority until we drain them all return ProcessList.SCHED_GROUP_DEFAULT; - } else if ((mActive != null) - && (mActive.isForeground() || mActive.ordered || mActive.alarm)) { - // We have an important broadcast right now, so boost priority + } else if ((mActive != null) && mActive.isForeground()) { + // We have a foreground broadcast right now, so boost priority return ProcessList.SCHED_GROUP_DEFAULT; } else if (!isIdle()) { return ProcessList.SCHED_GROUP_BACKGROUND; @@ -309,7 +378,7 @@ class BroadcastProcessQueue { */ public void makeActiveNextPending() { // TODO: what if the next broadcast isn't runnable yet? - final SomeArgs next = mPending.removeFirst(); + final SomeArgs next = removeNextBroadcast(); mActive = (BroadcastRecord) next.arg1; mActiveIndex = next.argi1; mActiveBlockedUntilTerminalCount = next.argi2; @@ -347,6 +416,15 @@ class BroadcastProcessQueue { if (record.prioritized) { mCountPrioritized++; } + if (record.interactive) { + mCountInteractive++; + } + if (record.resultTo != null) { + mCountResultTo++; + } + if (record.callerInstrumented) { + mCountInstrumented++; + } invalidateRunnableAt(); } @@ -366,6 +444,15 @@ class BroadcastProcessQueue { if (record.prioritized) { mCountPrioritized--; } + if (record.interactive) { + mCountInteractive--; + } + if (record.resultTo != null) { + mCountResultTo--; + } + if (record.callerInstrumented) { + mCountInstrumented--; + } invalidateRunnableAt(); } @@ -413,7 +500,7 @@ class BroadcastProcessQueue { } public boolean isEmpty() { - return mPending.isEmpty(); + return mPending.isEmpty() && mPendingUrgent.isEmpty(); } public boolean isActive() { @@ -421,6 +508,38 @@ class BroadcastProcessQueue { } /** + * Will thrown an exception if there are no pending broadcasts; relies on + * {@link #isEmpty()} being false. + */ + SomeArgs removeNextBroadcast() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return queue.removeFirst(); + } + + @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() { + if (!mPendingUrgent.isEmpty()) { + return mPendingUrgent; + } else if (!mPending.isEmpty()) { + return mPending; + } + return null; + } + + /** + * Returns null if there are no pending broadcasts + */ + @Nullable SomeArgs peekNextBroadcast() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return (queue != null) ? queue.peekFirst() : null; + } + + @VisibleForTesting + @Nullable BroadcastRecord peekNextBroadcastRecord() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null; + } + + /** * Quickly determine if this queue has broadcasts that are still waiting to * be delivered at some point in the future. */ @@ -437,11 +556,13 @@ class BroadcastProcessQueue { return mActive.enqueueTime > barrierTime; } final SomeArgs next = mPending.peekFirst(); - if (next != null) { - return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; - } - // Nothing running or runnable means we're past the barrier - return true; + final SomeArgs nextUrgent = mPendingUrgent.peekFirst(); + // Empty queue is past any barrier + final boolean nextLater = next == null + || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; + final boolean nextUrgentLater = nextUrgent == null + || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime; + return nextLater && nextUrgentLater; } public boolean isRunnable() { @@ -477,25 +598,33 @@ class BroadcastProcessQueue { } static final int REASON_EMPTY = 0; - static final int REASON_CONTAINS_FOREGROUND = 1; - static final int REASON_CONTAINS_ORDERED = 2; - static final int REASON_CONTAINS_ALARM = 3; - static final int REASON_CONTAINS_PRIORITIZED = 4; - static final int REASON_CACHED = 5; - static final int REASON_NORMAL = 6; - static final int REASON_MAX_PENDING = 7; - static final int REASON_BLOCKED = 8; + static final int REASON_CACHED = 1; + static final int REASON_NORMAL = 2; + static final int REASON_MAX_PENDING = 3; + static final int REASON_BLOCKED = 4; + static final int REASON_INSTRUMENTED = 5; + static final int REASON_CONTAINS_FOREGROUND = 10; + static final int REASON_CONTAINS_ORDERED = 11; + static final int REASON_CONTAINS_ALARM = 12; + static final int REASON_CONTAINS_PRIORITIZED = 13; + static final int REASON_CONTAINS_INTERACTIVE = 14; + static final int REASON_CONTAINS_RESULT_TO = 15; + static final int REASON_CONTAINS_INSTRUMENTED = 16; @IntDef(flag = false, prefix = { "REASON_" }, value = { REASON_EMPTY, - REASON_CONTAINS_FOREGROUND, - REASON_CONTAINS_ORDERED, - REASON_CONTAINS_ALARM, - REASON_CONTAINS_PRIORITIZED, REASON_CACHED, REASON_NORMAL, REASON_MAX_PENDING, REASON_BLOCKED, + REASON_INSTRUMENTED, + REASON_CONTAINS_FOREGROUND, + REASON_CONTAINS_ORDERED, + REASON_CONTAINS_ALARM, + REASON_CONTAINS_PRIORITIZED, + REASON_CONTAINS_INTERACTIVE, + REASON_CONTAINS_RESULT_TO, + REASON_CONTAINS_INSTRUMENTED, }) @Retention(RetentionPolicy.SOURCE) public @interface Reason {} @@ -503,14 +632,18 @@ class BroadcastProcessQueue { static @NonNull String reasonToString(@Reason int reason) { switch (reason) { case REASON_EMPTY: return "EMPTY"; - case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; - case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; - case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; - case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED"; case REASON_CACHED: return "CACHED"; case REASON_NORMAL: return "NORMAL"; case REASON_MAX_PENDING: return "MAX_PENDING"; case REASON_BLOCKED: return "BLOCKED"; + case REASON_INSTRUMENTED: return "INSTRUMENTED"; + case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; + case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; + case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; + case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED"; + case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE"; + case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO"; + case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED"; default: return Integer.toString(reason); } } @@ -519,7 +652,7 @@ class BroadcastProcessQueue { * Update {@link #getRunnableAt()} if it's currently invalidated. */ private void updateRunnableAt() { - final SomeArgs next = mPending.peekFirst(); + final SomeArgs next = peekNextBroadcast(); if (next != null) { final BroadcastRecord r = (BroadcastRecord) next.arg1; final int index = next.argi1; @@ -537,7 +670,7 @@ class BroadcastProcessQueue { // If we have too many broadcasts pending, bypass any delays that // might have been applied above to aid draining - if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) { + if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_MAX_PENDING; return; @@ -555,6 +688,18 @@ class BroadcastProcessQueue { } else if (mCountPrioritized > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; + } else if (mCountInteractive > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_INTERACTIVE; + } else if (mCountResultTo > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; + } else if (mCountInstrumented > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED; + } else if (mProcessInstrumented) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_INSTRUMENTED; } else if (mProcessCached) { mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; mRunnableAtReason = REASON_CACHED; @@ -574,8 +719,8 @@ class BroadcastProcessQueue { */ public void checkHealthLocked() { if (mRunnableAtReason == REASON_BLOCKED) { - final SomeArgs next = mPending.peekFirst(); - Objects.requireNonNull(next, "peekFirst"); + final SomeArgs next = peekNextBroadcast(); + Objects.requireNonNull(next, "peekNextBroadcast"); // If blocked more than 10 minutes, we're likely wedged final BroadcastRecord r = (BroadcastRecord) next.arg1; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index e421c61a2bd6..db3ef3d51b16 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -441,7 +441,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // relevant per-process queue final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { - queue.app = app; + queue.setProcess(app); } boolean didSomething = false; @@ -478,7 +478,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // relevant per-process queue final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { - queue.app = null; + queue.setProcess(null); } if ((mRunningColdStart != null) && (mRunningColdStart == queue)) { @@ -816,19 +816,21 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } final BroadcastRecord r = queue.getActive(); - r.resultCode = resultCode; - r.resultData = resultData; - r.resultExtras = resultExtras; - if (!r.isNoAbort()) { - r.resultAbort = resultAbort; - } - - // When the caller aborted an ordered broadcast, we mark all remaining - // receivers as skipped - if (r.ordered && r.resultAbort) { - for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { - setDeliveryState(null, null, r, i, r.receivers.get(i), - BroadcastRecord.DELIVERY_SKIPPED); + if (r.ordered) { + r.resultCode = resultCode; + r.resultData = resultData; + r.resultExtras = resultExtras; + if (!r.isNoAbort()) { + r.resultAbort = resultAbort; + } + + // When the caller aborted an ordered broadcast, we mark all + // remaining receivers as skipped + if (r.resultAbort) { + for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { + setDeliveryState(null, null, r, i, r.receivers.get(i), + BroadcastRecord.DELIVERY_SKIPPED); + } } } @@ -925,7 +927,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { notifyFinishReceiver(queue, r, index, receiver); // When entire ordered broadcast finished, deliver final result - if (r.ordered && (r.terminalCount == r.receivers.size())) { + final boolean recordFinished = (r.terminalCount == r.receivers.size()); + if (recordFinished) { scheduleResultTo(r); } @@ -1217,7 +1220,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) { if (!queue.isProcessWarm()) { - queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid); + queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid)); } } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 4f640033d1a4..d7dc8b80931b 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -78,11 +78,13 @@ final class BroadcastRecord extends Binder { final int callingPid; // the pid of who sent this final int callingUid; // the uid of who sent this final boolean callerInstantApp; // caller is an Instant App? + final boolean callerInstrumented; // caller is being instrumented final boolean ordered; // serialize the send to receivers? final boolean sticky; // originated from existing sticky data? final boolean alarm; // originated from an alarm triggering? final boolean pushMessage; // originated from a push message? final boolean pushMessageOverQuota; // originated from a push message which was over quota? + final boolean interactive; // originated from user interaction? final boolean initialSticky; // initial broadcast from register to sticky? final boolean prioritized; // contains more than one priority tranche final int userId; // user id this broadcast was for @@ -364,6 +366,8 @@ final class BroadcastRecord extends Binder { callingPid = _callingPid; callingUid = _callingUid; callerInstantApp = _callerInstantApp; + callerInstrumented = (_callerApp != null) + ? (_callerApp.getActiveInstrumentation() != null) : false; resolvedType = _resolvedType; requiredPermissions = _requiredPermissions; excludedPermissions = _excludedPermissions; @@ -392,6 +396,7 @@ final class BroadcastRecord extends Binder { alarm = options != null && options.isAlarmBroadcast(); pushMessage = options != null && options.isPushMessagingBroadcast(); pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast(); + interactive = options != null && options.isInteractiveBroadcast(); this.filterExtrasForReceiver = filterExtrasForReceiver; } @@ -409,6 +414,7 @@ final class BroadcastRecord extends Binder { callingPid = from.callingPid; callingUid = from.callingUid; callerInstantApp = from.callerInstantApp; + callerInstrumented = from.callerInstrumented; ordered = from.ordered; sticky = from.sticky; initialSticky = from.initialSticky; @@ -450,6 +456,7 @@ final class BroadcastRecord extends Binder { alarm = from.alarm; pushMessage = from.pushMessage; pushMessageOverQuota = from.pushMessageOverQuota; + interactive = from.interactive; filterExtrasForReceiver = from.filterExtrasForReceiver; } @@ -611,6 +618,18 @@ final class BroadcastRecord extends Binder { return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0; } + /** + * Core policy determination about this broadcast's delivery prioritization + */ + boolean isUrgent() { + // TODO: flags for controlling policy + // TODO: migrate alarm-prioritization flag to BroadcastConstants + return (isForeground() + || interactive + || alarm) + && receivers.size() == 1; + } + @NonNull String getHostingRecordTriggerType() { if (alarm) { return HostingRecord.TRIGGER_TYPE_ALARM; diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 975619feaea0..740efbc658ba 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -443,13 +443,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub { // invocation side effects such as allowlisting. if (options != null && callingUid != Process.SYSTEM_UID && key.type == ActivityManager.INTENT_SENDER_BROADCAST) { - if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) { + if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) + || options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { if (DEBUG_BROADCAST_LIGHT) { Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm-related"); + + " may not flag broadcast as alarm or interactive"); } throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm-related"); + "Non-system callers may not flag broadcasts as alarm or interactive"); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java index 1370fd83f6a8..da7781add8c6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java @@ -21,6 +21,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.BiometricConstants; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -293,4 +294,30 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { + ", requestId=" + getRequestId() + ", userId=" + getTargetUserId() + "}"; } + + /** + * Cancels this ClientMonitor + */ + public void cancel() { + cancelWithoutStarting(mCallback); + } + + /** + * Cancels this ClientMonitor without starting + * @param callback + */ + public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) { + Slog.d(TAG, "cancelWithoutStarting: " + this); + + final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED; + try { + ClientMonitorCallbackConverter listener = getListener(); + if (listener != null) { + listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to invoke sendError", e); + } + callback.onClientFinished(this, true /* success */); + } } 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 9317c4ec12b5..fb978b2ba4b9 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -543,4 +543,37 @@ public class BiometricScheduler { mPendingOperations.clear(); mCurrentOperation = null; } + + /** + * Marks all pending operations as canceling and cancels the current + * operation. + */ + private void clearScheduler() { + if (mCurrentOperation == null) { + return; + } + for (BiometricSchedulerOperation pendingOperation : mPendingOperations) { + Slog.d(getTag(), "[Watchdog cancelling pending] " + + pendingOperation.getClientMonitor()); + pendingOperation.markCanceling(); + } + Slog.d(getTag(), "[Watchdog cancelling current] " + + mCurrentOperation.getClientMonitor()); + mCurrentOperation.cancel(mHandler, getInternalCallback()); + } + + /** + * Start the timeout for the watchdog. + */ + public void startWatchdog() { + if (mCurrentOperation == null) { + return; + } + final BiometricSchedulerOperation mOperation = mCurrentOperation; + mHandler.postDelayed(() -> { + if (mOperation == mCurrentOperation) { + clearScheduler(); + } + }, 10000); + } } 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 ef2931ff5850..dacec38b0e7e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -267,7 +267,7 @@ public class BiometricSchedulerOperation { /** Flags this operation as canceled, if possible, but does not cancel it until started. */ public boolean markCanceling() { - if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) { + if (mState == STATE_WAITING_IN_QUEUE) { mState = STATE_WAITING_IN_QUEUE_CANCELING; return true; } @@ -287,10 +287,6 @@ public class BiometricSchedulerOperation { } final int currentState = mState; - if (!isInterruptable()) { - Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this); - return; - } if (currentState == STATE_STARTED_CANCELING) { Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this); return; @@ -301,10 +297,10 @@ public class BiometricSchedulerOperation { || currentState == STATE_WAITING_IN_QUEUE_CANCELING || currentState == STATE_WAITING_FOR_COOKIE) { Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor); - ((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback)); + mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback)); } else { Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor); - ((Interruptable) mClientMonitor).cancel(); + mClientMonitor.cancel(); } // forcibly finish this client if the HAL does not acknowledge within the timeout 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 271bce9890c6..2761ec04aa7e 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 @@ -183,6 +183,18 @@ public class FaceService extends SystemService { receiver, opPackageName, disabledFeatures, previewSurface, debugConsent); } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void scheduleWatchdog() { + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); + if (provider == null) { + Slog.w(TAG, "Null provider for scheduling watchdog"); + return; + } + + provider.second.scheduleWatchdog(provider.first); + } + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC) @Override // Binder call public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken, diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java index 4efaedbd5530..85f95cec8377 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java @@ -128,4 +128,10 @@ public interface ServiceProvider extends BiometricServiceProvider<FaceSensorProp @NonNull String opPackageName); void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args); + + /** + * Schedules watchdog for canceling hung operations + * @param sensorId sensor ID of the associated operation + */ + default void scheduleWatchdog(int sensorId) {} } 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 b60f9d80d425..c12994c993e6 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 @@ -52,6 +52,7 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.InvalidationRequesterClient; @@ -661,4 +662,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { void setTestHalEnabled(boolean enabled) { mTestHalEnabled = enabled; } + + @Override + public void scheduleWatchdog(int sensorId) { + Slog.d(getTag(), "Starting watchdog for face"); + final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + if (biometricScheduler == null) { + return; + } + biometricScheduler.startWatchdog(); + } } 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 7e2742edd47a..b0dc28ddce96 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 @@ -879,6 +879,18 @@ public class FingerprintService extends SystemService { provider.onPowerPressed(); } } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void scheduleWatchdog() { + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); + if (provider == null) { + Slog.w(TAG, "Null provider for scheduling watchdog"); + return; + } + + provider.second.scheduleWatchdog(provider.first); + } }; public FingerprintService(Context context) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 9075e7ec2080..0c29f5615b4c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -140,4 +140,10 @@ public interface ServiceProvider extends @NonNull ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName); + + /** + * Schedules watchdog for canceling hung operations + * @param sensorId sensor ID of the associated operation + */ + default void scheduleWatchdog(int sensorId) {} } 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 650894db431a..17ba07f2c2bd 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 com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; 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.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; @@ -779,4 +780,14 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } return null; } + + @Override + public void scheduleWatchdog(int sensorId) { + Slog.d(getTag(), "Starting watchdog for fingerprint"); + final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + if (biometricScheduler == null) { + return; + } + biometricScheduler.startWatchdog(); + } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 587db41c0df8..5eb15e09f09e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -40,6 +40,7 @@ import static android.os.Process.ROOT_UID; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.compat.CompatChanges; @@ -101,6 +102,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.provider.DeviceConfig; import android.provider.Settings; import android.sysprop.DisplayProperties; import android.text.TextUtils; @@ -202,8 +204,6 @@ public final class DisplayManagerService extends SystemService { private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable"; private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top"; - private static final String PROP_USE_NEW_DISPLAY_POWER_CONTROLLER = - "persist.sys.use_new_display_power_controller"; private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000; // This value needs to be in sync with the threshold // in RefreshRateConfigs::getFrameRateDivisor. @@ -1356,11 +1356,19 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { synchronized (mSyncRoot) { - final int displayId = createVirtualDisplayLocked(callback, projection, callingUid, - packageName, surface, flags, virtualDisplayConfig); + final int displayId = + createVirtualDisplayLocked( + callback, + projection, + callingUid, + packageName, + virtualDevice, + surface, + flags, + virtualDisplayConfig); if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) { - mDisplayWindowPolicyControllers.put(displayId, - Pair.create(virtualDevice, dwpc)); + mDisplayWindowPolicyControllers.put( + displayId, Pair.create(virtualDevice, dwpc)); } return displayId; } @@ -1369,12 +1377,20 @@ public final class DisplayManagerService extends SystemService { } } - private int createVirtualDisplayLocked(IVirtualDisplayCallback callback, - IMediaProjection projection, int callingUid, String packageName, Surface surface, - int flags, VirtualDisplayConfig virtualDisplayConfig) { + private int createVirtualDisplayLocked( + IVirtualDisplayCallback callback, + IMediaProjection projection, + int callingUid, + String packageName, + IVirtualDevice virtualDevice, + Surface surface, + int flags, + VirtualDisplayConfig virtualDisplayConfig) { if (mVirtualDisplayAdapter == null) { - Slog.w(TAG, "Rejecting request to create private virtual display " - + "because the virtual display adapter is not available."); + Slog.w( + TAG, + "Rejecting request to create private virtual display " + + "because the virtual display adapter is not available."); return -1; } @@ -1385,6 +1401,19 @@ public final class DisplayManagerService extends SystemService { return -1; } + // If the display is to be added to a device display group, we need to make the + // LogicalDisplayMapper aware of the link between the new display and its associated virtual + // device before triggering DISPLAY_DEVICE_EVENT_ADDED. + if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) { + try { + final int virtualDeviceId = virtualDevice.getDeviceId(); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice( + device, virtualDeviceId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + // DisplayDevice events are handled manually for Virtual Displays. // TODO: multi-display Fix this so that generic add/remove events are not handled in a // different code path for virtual displays. Currently this happens so that we can @@ -1393,8 +1422,7 @@ public final class DisplayManagerService extends SystemService { // called on the DisplayThread (which we don't want to wait for?). // One option would be to actually wait here on the binder thread // to be notified when the virtual display is created (or failed). - mDisplayDeviceRepo.onDisplayDeviceEvent(device, - DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED); + mDisplayDeviceRepo.onDisplayDeviceEvent(device, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED); final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display != null) { @@ -2575,6 +2603,7 @@ public final class DisplayManagerService extends SystemService { mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked); } + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) private void addDisplayPowerControllerLocked(LogicalDisplay display) { if (mPowerHandler == null) { // initPowerManagement has not yet been called. @@ -2588,7 +2617,8 @@ public final class DisplayManagerService extends SystemService { display, mSyncRoot); final DisplayPowerControllerInterface displayPowerController; - if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) { + if (DeviceConfig.getBoolean("display_manager", + "use_newly_structured_display_power_controller", false)) { displayPowerController = new DisplayPowerController2( mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler, mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting, diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 70c9e23c6af8..cb97e2832854 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -28,6 +28,7 @@ import android.os.PowerManager; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -123,6 +124,12 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { /** Map of all display groups indexed by display group id. */ private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>(); + /** + * Map of display groups which are linked to virtual devices (all displays in the group are + * linked to that device). Keyed by virtual device unique id. + */ + private final SparseIntArray mDeviceDisplayGroupIds = new SparseIntArray(); + private final DisplayDeviceRepository mDisplayDeviceRepo; private final DeviceStateToLayoutMap mDeviceStateToLayoutMap; private final Listener mListener; @@ -157,6 +164,12 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { */ private final SparseIntArray mDisplayGroupsToUpdate = new SparseIntArray(); + /** + * ArrayMap of display device unique ID to virtual device ID. Used in {@link + * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays. + */ + private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>(); + private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; private Layout mCurrentLayout = null; private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE; @@ -362,6 +375,19 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mDeviceStateToLayoutMap.dumpLocked(ipw); } + /** + * Creates an association between a displayDevice and a virtual device. Any displays associated + * with this virtual device will be grouped together in a single {@link DisplayGroup} unless + * created with {@link Display.FLAG_OWN_DISPLAY_GROUP}. + * + * @param displayDevice the displayDevice to be linked + * @param virtualDeviceUniqueId the unique ID of the virtual device. + */ + void associateDisplayDeviceWithVirtualDevice( + DisplayDevice displayDevice, int virtualDeviceUniqueId) { + mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId); + } + void setDeviceStateLocked(int state, boolean isOverrideActive) { Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState + ", interactive=" + mInteractive); @@ -556,6 +582,9 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked(); + // Remove any virtual device mapping which exists for the display. + mVirtualDeviceDisplayMapping.remove(device.getUniqueId()); + if (layoutDisplay.getAddress().equals(deviceInfo.address)) { layout.removeDisplayLocked(DEFAULT_DISPLAY); @@ -749,24 +778,44 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // We wait until we sent the EVENT_REMOVED event before actually removing the // group. mDisplayGroups.delete(id); + // Remove possible reference to the removed group. + int deviceIndex = mDeviceDisplayGroupIds.indexOfValue(id); + if (deviceIndex >= 0) { + mDeviceDisplayGroupIds.removeAt(deviceIndex); + } } } } private void assignDisplayGroupLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); + final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId(); + final Integer linkedDeviceUniqueId = + mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId); // Get current display group data int groupId = getDisplayGroupIdFromDisplayIdLocked(displayId); + Integer deviceDisplayGroupId = null; + if (linkedDeviceUniqueId != null + && mDeviceDisplayGroupIds.indexOfKey(linkedDeviceUniqueId) > 0) { + deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId); + } final DisplayGroup oldGroup = getDisplayGroupLocked(groupId); // Get the new display group if a change is needed final DisplayInfo info = display.getDisplayInfoLocked(); final boolean needsOwnDisplayGroup = (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0; final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP; + final boolean needsDeviceDisplayGroup = + !needsOwnDisplayGroup && linkedDeviceUniqueId != null; + final boolean hasDeviceDisplayGroup = + deviceDisplayGroupId != null && groupId == deviceDisplayGroupId; if (groupId == Display.INVALID_DISPLAY_GROUP - || hasOwnDisplayGroup != needsOwnDisplayGroup) { - groupId = assignDisplayGroupIdLocked(needsOwnDisplayGroup); + || hasOwnDisplayGroup != needsOwnDisplayGroup + || hasDeviceDisplayGroup != needsDeviceDisplayGroup) { + groupId = + assignDisplayGroupIdLocked( + needsOwnDisplayGroup, needsDeviceDisplayGroup, linkedDeviceUniqueId); } // Create a new group if needed @@ -931,7 +980,17 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { display.setPhase(phase); } - private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) { + private int assignDisplayGroupIdLocked( + boolean isOwnDisplayGroup, boolean isDeviceDisplayGroup, Integer linkedDeviceUniqueId) { + if (isDeviceDisplayGroup && linkedDeviceUniqueId != null) { + int deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId); + // A value of 0 indicates that no device display group was found. + if (deviceDisplayGroupId == 0) { + deviceDisplayGroupId = mNextNonDefaultGroupId++; + mDeviceDisplayGroupIds.put(linkedDeviceUniqueId, deviceDisplayGroupId); + } + return deviceDisplayGroupId; + } return isOwnDisplayGroup ? mNextNonDefaultGroupId++ : Display.DEFAULT_DISPLAY_GROUP; } diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index 819b719dd22e..cd9ef0915741 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -42,6 +42,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; import java.util.NoSuchElementException; /** @@ -62,8 +64,6 @@ final class DreamController { private final Handler mHandler; private final Listener mListener; private final ActivityTaskManager mActivityTaskManager; - private long mDreamStartTime; - private String mSavedStopReason; private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); @@ -74,21 +74,15 @@ final class DreamController { private DreamRecord mCurrentDream; - private final Runnable mStopUnconnectedDreamRunnable = new Runnable() { - @Override - public void run() { - if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) { - Slog.w(TAG, "Bound dream did not connect in the time allotted"); - stopDream(true /*immediate*/, "slow to connect"); - } - } - }; + // Whether a dreaming started intent has been broadcast. + private boolean mSentStartBroadcast = false; - private final Runnable mStopStubbornDreamRunnable = () -> { - Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); - stopDream(true /*immediate*/, "slow to finish"); - mSavedStopReason = null; - }; + // When a new dream is started and there is an existing dream, the existing dream is allowed to + // live a little longer until the new dream is started, for a smoother transition. This dream is + // stopped as soon as the new dream is started, and this list is cleared. Usually there should + // only be one previous dream while waiting for a new dream to start, but we store a list to + // proof the edge case of multiple previous dreams. + private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>(); public DreamController(Context context, Handler handler, Listener listener) { mContext = context; @@ -110,18 +104,17 @@ final class DreamController { pw.println(" mUserId=" + mCurrentDream.mUserId); pw.println(" mBound=" + mCurrentDream.mBound); pw.println(" mService=" + mCurrentDream.mService); - pw.println(" mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast); pw.println(" mWakingGently=" + mCurrentDream.mWakingGently); } else { pw.println(" mCurrentDream: null"); } + + pw.println(" mSentStartBroadcast=" + mSentStartBroadcast); } public void startDream(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock, ComponentName overlayComponentName, String reason) { - stopDream(true /*immediate*/, "starting new dream"); - Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream"); try { // Close the notification shade. No need to send to all, but better to be explicit. @@ -131,9 +124,12 @@ final class DreamController { + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze + ", userId=" + userId + ", reason='" + reason + "'"); + if (mCurrentDream != null) { + mPreviousDreams.add(mCurrentDream); + } mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock); - mDreamStartTime = SystemClock.elapsedRealtime(); + mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime(); MetricsLogger.visible(mContext, mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); @@ -156,31 +152,49 @@ final class DreamController { } mCurrentDream.mBound = true; - mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT); + mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable, + DREAM_CONNECTION_TIMEOUT); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops dreaming. + * + * The current dream, if any, and any unstopped previous dreams are stopped. The device stops + * dreaming. + */ public void stopDream(boolean immediate, String reason) { - if (mCurrentDream == null) { + stopPreviousDreams(); + stopDreamInstance(immediate, reason, mCurrentDream); + } + + /** + * Stops the given dream instance. + * + * The device may still be dreaming afterwards if there are other dreams running. + */ + private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) { + if (dream == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream"); try { if (!immediate) { - if (mCurrentDream.mWakingGently) { + if (dream.mWakingGently) { return; // already waking gently } - if (mCurrentDream.mService != null) { + if (dream.mService != null) { // Give the dream a moment to wake up and finish itself gently. - mCurrentDream.mWakingGently = true; + dream.mWakingGently = true; try { - mSavedStopReason = reason; - mCurrentDream.mService.wakeUp(); - mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT); + dream.mStopReason = reason; + dream.mService.wakeUp(); + mHandler.postDelayed(dream.mStopStubbornDreamRunnable, + DREAM_FINISH_TIMEOUT); return; } catch (RemoteException ex) { // oh well, we tried, finish immediately instead @@ -188,56 +202,76 @@ final class DreamController { } } - final DreamRecord oldDream = mCurrentDream; - mCurrentDream = null; - Slog.i(TAG, "Stopping dream: name=" + oldDream.mName - + ", isPreviewMode=" + oldDream.mIsPreviewMode - + ", canDoze=" + oldDream.mCanDoze - + ", userId=" + oldDream.mUserId + Slog.i(TAG, "Stopping dream: name=" + dream.mName + + ", isPreviewMode=" + dream.mIsPreviewMode + + ", canDoze=" + dream.mCanDoze + + ", userId=" + dream.mUserId + ", reason='" + reason + "'" - + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')")); + + (dream.mStopReason == null ? "" : "(from '" + + dream.mStopReason + "')")); MetricsLogger.hidden(mContext, - oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); + dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); MetricsLogger.histogram(mContext, - oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" , - (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L))); + dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes", + (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L + * 60L))); - mHandler.removeCallbacks(mStopUnconnectedDreamRunnable); - mHandler.removeCallbacks(mStopStubbornDreamRunnable); - mSavedStopReason = null; + mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable); + mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable); - if (oldDream.mSentStartBroadcast) { - mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); - } - - if (oldDream.mService != null) { + if (dream.mService != null) { try { - oldDream.mService.detach(); + dream.mService.detach(); } catch (RemoteException ex) { // we don't care; this thing is on the way out } try { - oldDream.mService.asBinder().unlinkToDeath(oldDream, 0); + dream.mService.asBinder().unlinkToDeath(dream, 0); } catch (NoSuchElementException ex) { // don't care } - oldDream.mService = null; + dream.mService = null; } - if (oldDream.mBound) { - mContext.unbindService(oldDream); + if (dream.mBound) { + mContext.unbindService(dream); } - oldDream.releaseWakeLockIfNeeded(); + dream.releaseWakeLockIfNeeded(); + + // Current dream stopped, device no longer dreaming. + if (dream == mCurrentDream) { + mCurrentDream = null; + + if (mSentStartBroadcast) { + mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + } - mActivityTaskManager.removeRootTasksWithActivityTypes(new int[] {ACTIVITY_TYPE_DREAM}); + mActivityTaskManager.removeRootTasksWithActivityTypes( + new int[] {ACTIVITY_TYPE_DREAM}); - mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); + mListener.onDreamStopped(dream.mToken); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops all previous dreams, if any. + */ + private void stopPreviousDreams() { + if (mPreviousDreams.isEmpty()) { + return; + } + + // Using an iterator because mPreviousDreams is modified while the iteration is in process. + for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) { + stopDreamInstance(true /*immediate*/, "stop previous dream", it.next()); + it.remove(); + } + } + private void attach(IDreamService service) { try { service.asBinder().linkToDeath(mCurrentDream, 0); @@ -251,9 +285,9 @@ final class DreamController { mCurrentDream.mService = service; - if (!mCurrentDream.mIsPreviewMode) { + if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL); - mCurrentDream.mSentStartBroadcast = true; + mSentStartBroadcast = true; } } @@ -275,10 +309,35 @@ final class DreamController { public boolean mBound; public boolean mConnected; public IDreamService mService; - public boolean mSentStartBroadcast; - + private String mStopReason; + private long mDreamStartTime; public boolean mWakingGently; + private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded; + private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; + + private final Runnable mStopUnconnectedDreamRunnable = () -> { + if (mBound && !mConnected) { + Slog.w(TAG, "Bound dream did not connect in the time allotted"); + stopDream(true /*immediate*/, "slow to connect" /*reason*/); + } + }; + + private final Runnable mStopStubbornDreamRunnable = () -> { + Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); + stopDream(true /*immediate*/, "slow to finish" /*reason*/); + mStopReason = null; + }; + + private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { + // May be called on any thread. + @Override + public void sendResult(Bundle data) { + mHandler.post(mStopPreviousDreamsIfNeeded); + mHandler.post(mReleaseWakeLockIfNeeded); + } + }; + DreamRecord(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) { mToken = token; @@ -289,7 +348,9 @@ final class DreamController { mWakeLock = wakeLock; // Hold the lock while we're waiting for the service to connect and start dreaming. // Released after the service has started dreaming, we stop dreaming, or it timed out. - mWakeLock.acquire(); + if (mWakeLock != null) { + mWakeLock.acquire(); + } mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } @@ -329,6 +390,12 @@ final class DreamController { }); } + void stopPreviousDreamsIfNeeded() { + if (mCurrentDream == DreamRecord.this) { + stopPreviousDreams(); + } + } + void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); @@ -336,15 +403,5 @@ final class DreamController { mHandler.removeCallbacks(mReleaseWakeLockIfNeeded); } } - - final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; - - final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { - // May be called on any thread. - @Override - public void sendResult(Bundle data) throws RemoteException { - mHandler.post(mReleaseWakeLockIfNeeded); - } - }; } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 951a8a249f87..6e2ccebb6ff4 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -493,8 +493,6 @@ public final class DreamManagerService extends SystemService { return; } - stopDreamLocked(true /*immediate*/, "starting new dream"); - Slog.i(TAG, "Entering dreamland."); mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze); diff --git a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java index 5253d34a38f0..d4e8f27a7b34 100644 --- a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java +++ b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java @@ -19,28 +19,9 @@ import android.annotation.ArrayRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; -import android.annotation.UserIdInt; -import android.app.AppGlobals; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.TimeUtils; - -import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; /** * Gets the service name using a framework resources, temporarily changing the service if necessary @@ -48,259 +29,42 @@ import java.util.List; * * @hide */ -public final class FrameworkResourcesServiceNameResolver implements ServiceNameResolver { - - private static final String TAG = FrameworkResourcesServiceNameResolver.class.getSimpleName(); - - /** Handler message to {@link #resetTemporaryService(int)} */ - private static final int MSG_RESET_TEMPORARY_SERVICE = 0; +public final class FrameworkResourcesServiceNameResolver extends ServiceNameBaseResolver { - @NonNull - private final Context mContext; - @NonNull - private final Object mLock = new Object(); - @StringRes private final int mStringResourceId; @ArrayRes private final int mArrayResourceId; - private final boolean mIsMultiple; - /** - * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)}, - * keyed by {@code userId}. - * - * <p>Typically used by Shell command and/or CTS tests to configure temporary services if - * mIsMultiple is true. - */ - @GuardedBy("mLock") - private final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>(); - /** - * Map of default services that have been disabled by - * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}. - * - * <p>Typically used by Shell command and/or CTS tests. - */ - @GuardedBy("mLock") - private final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray(); - @Nullable - private NameResolverListener mOnSetCallback; - /** - * When the temporary service will expire (and reset back to the default). - */ - @GuardedBy("mLock") - private long mTemporaryServiceExpiration; - - /** - * Handler used to reset the temporary service name. - */ - @GuardedBy("mLock") - private Handler mTemporaryHandler; public FrameworkResourcesServiceNameResolver(@NonNull Context context, @StringRes int resourceId) { - mContext = context; + super(context, false); mStringResourceId = resourceId; mArrayResourceId = -1; - mIsMultiple = false; } public FrameworkResourcesServiceNameResolver(@NonNull Context context, @ArrayRes int resourceId, boolean isMultiple) { + super(context, isMultiple); if (!isMultiple) { throw new UnsupportedOperationException("Please use " + "FrameworkResourcesServiceNameResolver(context, @StringRes int) constructor " + "if single service mode is requested."); } - mContext = context; mStringResourceId = -1; mArrayResourceId = resourceId; - mIsMultiple = true; - } - - @Override - public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) { - synchronized (mLock) { - this.mOnSetCallback = callback; - } - } - - @Override - public String getServiceName(@UserIdInt int userId) { - String[] serviceNames = getServiceNameList(userId); - return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; - } - - @Override - public String getDefaultServiceName(@UserIdInt int userId) { - String[] serviceNames = getDefaultServiceNameList(userId); - return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; - } - - /** - * Gets the default list of the service names for the given user. - * - * <p>Typically implemented by services which want to provide multiple backends. - */ - @Override - public String[] getServiceNameList(int userId) { - synchronized (mLock) { - String[] temporaryNames = mTemporaryServiceNamesList.get(userId); - if (temporaryNames != null) { - // Always log it, as it should only be used on CTS or during development - Slog.w(TAG, "getServiceName(): using temporary name " - + Arrays.toString(temporaryNames) + " for user " + userId); - return temporaryNames; - } - final boolean disabled = mDefaultServicesDisabled.get(userId); - if (disabled) { - // Always log it, as it should only be used on CTS or during development - Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for " - + "user " + userId); - return null; - } - return getDefaultServiceNameList(userId); - - } - } - - /** - * Gets the default list of the service names for the given user. - * - * <p>Typically implemented by services which want to provide multiple backends. - */ - @Override - public String[] getDefaultServiceNameList(int userId) { - synchronized (mLock) { - if (mIsMultiple) { - String[] serviceNameList = mContext.getResources().getStringArray(mArrayResourceId); - // Filter out unimplemented services - // Initialize the validated array as null because we do not know the final size. - List<String> validatedServiceNameList = new ArrayList<>(); - try { - for (int i = 0; i < serviceNameList.length; i++) { - if (TextUtils.isEmpty(serviceNameList[i])) { - continue; - } - ComponentName serviceComponent = ComponentName.unflattenFromString( - serviceNameList[i]); - ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); - if (serviceInfo != null) { - validatedServiceNameList.add(serviceNameList[i]); - } - } - } catch (Exception e) { - Slog.e(TAG, "Could not validate provided services.", e); - } - String[] validatedServiceNameArray = new String[validatedServiceNameList.size()]; - return validatedServiceNameList.toArray(validatedServiceNameArray); - } else { - final String name = mContext.getString(mStringResourceId); - return TextUtils.isEmpty(name) ? new String[0] : new String[]{name}; - } - } - } - - @Override - public boolean isConfiguredInMultipleMode() { - return mIsMultiple; - } - - @Override - public boolean isTemporary(@UserIdInt int userId) { - synchronized (mLock) { - return mTemporaryServiceNamesList.get(userId) != null; - } } @Override - public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName, - int durationMs) { - setTemporaryServices(userId, new String[]{componentName}, durationMs); - } - - @Override - public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) { - synchronized (mLock) { - mTemporaryServiceNamesList.put(userId, componentNames); - - if (mTemporaryHandler == null) { - mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { - synchronized (mLock) { - resetTemporaryService(userId); - } - } else { - Slog.wtf(TAG, "invalid handler msg: " + msg); - } - } - }; - } else { - mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); - } - mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs; - mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); - for (int i = 0; i < componentNames.length; i++) { - notifyTemporaryServiceNameChangedLocked(userId, componentNames[i], - /* isTemporary= */ true); - } - } - } - - @Override - public void resetTemporaryService(@UserIdInt int userId) { - synchronized (mLock) { - Slog.i(TAG, "resetting temporary service for user " + userId + " from " - + Arrays.toString(mTemporaryServiceNamesList.get(userId))); - mTemporaryServiceNamesList.remove(userId); - if (mTemporaryHandler != null) { - mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); - mTemporaryHandler = null; - } - notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null, - /* isTemporary= */ false); - } - } - - @Override - public boolean setDefaultServiceEnabled(int userId, boolean enabled) { - synchronized (mLock) { - final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId); - if (currentlyEnabled == enabled) { - Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled); - return false; - } - if (enabled) { - Slog.i(TAG, "disabling default service for user " + userId); - mDefaultServicesDisabled.removeAt(userId); - } else { - Slog.i(TAG, "enabling default service for user " + userId); - mDefaultServicesDisabled.put(userId, true); - } - } - return true; + public String[] readServiceNameList(int userId) { + return mContext.getResources().getStringArray(mArrayResourceId); } + @Nullable @Override - public boolean isDefaultServiceEnabled(int userId) { - synchronized (mLock) { - return isDefaultServiceEnabledLocked(userId); - } + public String readServiceName(int userId) { + return mContext.getResources().getString(mStringResourceId); } - private boolean isDefaultServiceEnabledLocked(int userId) { - return !mDefaultServicesDisabled.get(userId); - } - - @Override - public String toString() { - synchronized (mLock) { - return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]"; - } - } // TODO(b/117779333): support proto @Override @@ -314,31 +78,4 @@ public final class FrameworkResourcesServiceNameResolver implements ServiceNameR pw.print(mDefaultServicesDisabled.size()); } } - - // TODO(b/117779333): support proto - @Override - public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) { - synchronized (mLock) { - final String[] temporaryNames = mTemporaryServiceNamesList.get(userId); - if (temporaryNames != null) { - pw.print("tmpName="); - pw.print(Arrays.toString(temporaryNames)); - final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime(); - pw.print(" (expires in "); - TimeUtils.formatDuration(ttl, pw); - pw.print("), "); - } - pw.print("defaultName="); - pw.print(getDefaultServiceName(userId)); - final boolean disabled = mDefaultServicesDisabled.get(userId); - pw.println(disabled ? " (disabled)" : " (enabled)"); - } - } - - private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId, - @Nullable String newTemporaryName, boolean isTemporary) { - if (mOnSetCallback != null) { - mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary); - } - } } diff --git a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java index cac7f53aad66..17d75e600c36 100644 --- a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java +++ b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java @@ -19,8 +19,11 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.Context; import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; import java.io.PrintWriter; +import java.util.Set; /** * Gets the service name using a property from the {@link android.provider.Settings.Secure} @@ -28,21 +31,34 @@ import java.io.PrintWriter; * * @hide */ -public final class SecureSettingsServiceNameResolver implements ServiceNameResolver { +public final class SecureSettingsServiceNameResolver extends ServiceNameBaseResolver { + /** + * The delimiter to be used to parse the secure settings string. Services must make sure + * that this delimiter is used while adding component names to their secure setting property. + */ + private static final char COMPONENT_NAME_SEPARATOR = ':'; - private final @NonNull Context mContext; + private final TextUtils.SimpleStringSplitter mStringColonSplitter = + new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); @NonNull private final String mProperty; public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property) { - mContext = context; - mProperty = property; + this(context, property, /*isMultiple*/false); } - @Override - public String getDefaultServiceName(@UserIdInt int userId) { - return Settings.Secure.getStringForUser(mContext.getContentResolver(), mProperty, userId); + /** + * + * @param context the context required to retrieve the secure setting value + * @param property name of the secure setting key + * @param isMultiple true if the system service using this resolver needs to connect to + * multiple remote services, false otherwise + */ + public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property, + boolean isMultiple) { + super(context, isMultiple); + mProperty = property; } // TODO(b/117779333): support proto @@ -61,4 +77,34 @@ public final class SecureSettingsServiceNameResolver implements ServiceNameResol public String toString() { return "SecureSettingsServiceNameResolver[" + mProperty + "]"; } + + @Override + public String[] readServiceNameList(int userId) { + return parseColonDelimitedServiceNames( + Settings.Secure.getStringForUser( + mContext.getContentResolver(), mProperty, userId)); + } + + @Override + public String readServiceName(int userId) { + return Settings.Secure.getStringForUser( + mContext.getContentResolver(), mProperty, userId); + } + + private String[] parseColonDelimitedServiceNames(String serviceNames) { + final Set<String> delimitedServices = new ArraySet<>(); + if (!TextUtils.isEmpty(serviceNames)) { + final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(serviceNames); + while (splitter.hasNext()) { + final String str = splitter.next(); + if (TextUtils.isEmpty(str)) { + continue; + } + delimitedServices.add(str); + } + } + String[] delimitedServicesArray = new String[delimitedServices.size()]; + return delimitedServices.toArray(delimitedServicesArray); + } } diff --git a/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java new file mode 100644 index 000000000000..76ea05e36141 --- /dev/null +++ b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java @@ -0,0 +1,325 @@ +/* + * 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. + */ +package com.android.server.infra; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.TimeUtils; + +import com.android.internal.annotations.GuardedBy; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Gets the service name using a framework resources, temporarily changing the service if necessary + * (typically during CTS tests or service development). + * + * @hide + */ +public abstract class ServiceNameBaseResolver implements ServiceNameResolver { + + private static final String TAG = ServiceNameBaseResolver.class.getSimpleName(); + + /** Handler message to {@link #resetTemporaryService(int)} */ + private static final int MSG_RESET_TEMPORARY_SERVICE = 0; + + @NonNull + protected final Context mContext; + @NonNull + protected final Object mLock = new Object(); + + protected final boolean mIsMultiple; + /** + * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)}, + * keyed by {@code userId}. + * + * <p>Typically used by Shell command and/or CTS tests to configure temporary services if + * mIsMultiple is true. + */ + @GuardedBy("mLock") + protected final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>(); + /** + * Map of default services that have been disabled by + * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}. + * + * <p>Typically used by Shell command and/or CTS tests. + */ + @GuardedBy("mLock") + protected final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray(); + @Nullable + private NameResolverListener mOnSetCallback; + /** + * When the temporary service will expire (and reset back to the default). + */ + @GuardedBy("mLock") + private long mTemporaryServiceExpiration; + + /** + * Handler used to reset the temporary service name. + */ + @GuardedBy("mLock") + private Handler mTemporaryHandler; + + protected ServiceNameBaseResolver(Context context, boolean isMultiple) { + mContext = context; + mIsMultiple = isMultiple; + } + + @Override + public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) { + synchronized (mLock) { + this.mOnSetCallback = callback; + } + } + + @Override + public String getServiceName(@UserIdInt int userId) { + String[] serviceNames = getServiceNameList(userId); + return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; + } + + @Override + public String getDefaultServiceName(@UserIdInt int userId) { + String[] serviceNames = getDefaultServiceNameList(userId); + return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; + } + + /** + * Gets the default list of the service names for the given user. + * + * <p>Typically implemented by services which want to provide multiple backends. + */ + @Override + public String[] getServiceNameList(int userId) { + synchronized (mLock) { + String[] temporaryNames = mTemporaryServiceNamesList.get(userId); + if (temporaryNames != null) { + // Always log it, as it should only be used on CTS or during development + Slog.w(TAG, "getServiceName(): using temporary name " + + Arrays.toString(temporaryNames) + " for user " + userId); + return temporaryNames; + } + final boolean disabled = mDefaultServicesDisabled.get(userId); + if (disabled) { + // Always log it, as it should only be used on CTS or during development + Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for " + + "user " + userId); + return null; + } + return getDefaultServiceNameList(userId); + + } + } + + /** + * Base classes must override this to read from the desired config e.g. framework resource, + * secure settings etc. + */ + @Nullable + public abstract String[] readServiceNameList(int userId); + + /** + * Base classes must override this to read from the desired config e.g. framework resource, + * secure settings etc. + */ + @Nullable + public abstract String readServiceName(int userId); + + /** + * Gets the default list of the service names for the given user. + * + * <p>Typically implemented by services which want to provide multiple backends. + */ + @Override + public String[] getDefaultServiceNameList(int userId) { + synchronized (mLock) { + if (mIsMultiple) { + String[] serviceNameList = readServiceNameList(userId); + // Filter out unimplemented services + // Initialize the validated array as null because we do not know the final size. + List<String> validatedServiceNameList = new ArrayList<>(); + try { + for (int i = 0; i < serviceNameList.length; i++) { + if (TextUtils.isEmpty(serviceNameList[i])) { + continue; + } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceNameList[i]); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); + if (serviceInfo != null) { + validatedServiceNameList.add(serviceNameList[i]); + } + } + } catch (Exception e) { + Slog.e(TAG, "Could not validate provided services.", e); + } + String[] validatedServiceNameArray = new String[validatedServiceNameList.size()]; + return validatedServiceNameList.toArray(validatedServiceNameArray); + } else { + final String name = readServiceName(userId); + return TextUtils.isEmpty(name) ? new String[0] : new String[]{name}; + } + } + } + + @Override + public boolean isConfiguredInMultipleMode() { + return mIsMultiple; + } + + @Override + public boolean isTemporary(@UserIdInt int userId) { + synchronized (mLock) { + return mTemporaryServiceNamesList.get(userId) != null; + } + } + + @Override + public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName, + int durationMs) { + setTemporaryServices(userId, new String[]{componentName}, durationMs); + } + + @Override + public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) { + synchronized (mLock) { + mTemporaryServiceNamesList.put(userId, componentNames); + + if (mTemporaryHandler == null) { + mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { + synchronized (mLock) { + resetTemporaryService(userId); + } + } else { + Slog.wtf(TAG, "invalid handler msg: " + msg); + } + } + }; + } else { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + } + mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs; + mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); + for (int i = 0; i < componentNames.length; i++) { + notifyTemporaryServiceNameChangedLocked(userId, componentNames[i], + /* isTemporary= */ true); + } + } + } + + @Override + public void resetTemporaryService(@UserIdInt int userId) { + synchronized (mLock) { + Slog.i(TAG, "resetting temporary service for user " + userId + " from " + + Arrays.toString(mTemporaryServiceNamesList.get(userId))); + mTemporaryServiceNamesList.remove(userId); + if (mTemporaryHandler != null) { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + mTemporaryHandler = null; + } + notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null, + /* isTemporary= */ false); + } + } + + @Override + public boolean setDefaultServiceEnabled(int userId, boolean enabled) { + synchronized (mLock) { + final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId); + if (currentlyEnabled == enabled) { + Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled); + return false; + } + if (enabled) { + Slog.i(TAG, "disabling default service for user " + userId); + mDefaultServicesDisabled.removeAt(userId); + } else { + Slog.i(TAG, "enabling default service for user " + userId); + mDefaultServicesDisabled.put(userId, true); + } + } + return true; + } + + @Override + public boolean isDefaultServiceEnabled(int userId) { + synchronized (mLock) { + return isDefaultServiceEnabledLocked(userId); + } + } + + @GuardedBy("mLock") + private boolean isDefaultServiceEnabledLocked(int userId) { + return !mDefaultServicesDisabled.get(userId); + } + + @Override + public String toString() { + synchronized (mLock) { + return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]"; + } + } + + // TODO(b/117779333): support proto + @Override + public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) { + synchronized (mLock) { + final String[] temporaryNames = mTemporaryServiceNamesList.get(userId); + if (temporaryNames != null) { + pw.print("tmpName="); + pw.print(Arrays.toString(temporaryNames)); + final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime(); + pw.print(" (expires in "); + TimeUtils.formatDuration(ttl, pw); + pw.print("), "); + } + pw.print("defaultName="); + pw.print(getDefaultServiceName(userId)); + final boolean disabled = mDefaultServicesDisabled.get(userId); + pw.println(disabled ? " (disabled)" : " (enabled)"); + } + } + + private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId, + @Nullable String newTemporaryName, boolean isTemporary) { + if (mOnSetCallback != null) { + mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary); + } + } +} diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 324eefc809e8..36199debaa6e 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -44,6 +44,8 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * A thread-safe component of {@link InputManagerService} responsible for managing the battery state @@ -63,6 +65,8 @@ final class BatteryController { @VisibleForTesting static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds + @VisibleForTesting + static final long USI_BATTERY_VALIDITY_DURATION_MILLIS = 60 * 60_000; // 1 hour private final Object mLock = new Object(); private final Context mContext; @@ -98,8 +102,12 @@ final class BatteryController { } public void systemRunning() { - Objects.requireNonNull(mContext.getSystemService(InputManager.class)) - .registerInputDeviceListener(mInputDeviceListener, mHandler); + final InputManager inputManager = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler); + for (int deviceId : inputManager.getInputDeviceIds()) { + mInputDeviceListener.onInputDeviceAdded(deviceId); + } } /** @@ -165,19 +173,20 @@ final class BatteryController { } } - @GuardedBy("mLock") - private void notifyAllListenersForDeviceLocked(State state) { - if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); - mListenerRecords.forEach((pid, listenerRecord) -> { - if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { - notifyBatteryListener(listenerRecord, state); - } - }); + private void notifyAllListenersForDevice(State state) { + synchronized (mLock) { + if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); + mListenerRecords.forEach((pid, listenerRecord) -> { + if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { + notifyBatteryListener(listenerRecord, state); + } + }); + } } @GuardedBy("mLock") private void updatePollingLocked(boolean delayStart) { - if (mDeviceMonitors.isEmpty() || !mIsInteractive) { + if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) { // Stop polling. mIsPolling = false; mHandler.removeCallbacks(this::handlePollEvent); @@ -192,6 +201,13 @@ final class BatteryController { mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0); } + private String getInputDeviceName(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null ? device.getName() : "<none>"; + } + private boolean hasBattery(int deviceId) { final InputDevice device = Objects.requireNonNull(mContext.getSystemService(InputManager.class)) @@ -199,6 +215,13 @@ final class BatteryController { return device != null && device.hasBattery(); } + private boolean isUsiDevice(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null && device.supportsUsi(); + } + @GuardedBy("mLock") private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) { return Objects.requireNonNull(mDeviceMonitors.get(deviceId), @@ -252,8 +275,10 @@ final class BatteryController { if (!hasRegisteredListenerForDeviceLocked(deviceId)) { // There are no more listeners monitoring this device. final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId); - monitor.stopMonitoring(); - mDeviceMonitors.remove(deviceId); + if (!monitor.isPersistent()) { + monitor.onMonitorDestroy(); + mDeviceMonitors.remove(deviceId); + } } if (listenerRecord.mMonitoredDevices.isEmpty()) { @@ -298,9 +323,7 @@ final class BatteryController { if (monitor == null) { return; } - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } + monitor.onUEvent(eventTime); } } @@ -310,18 +333,22 @@ final class BatteryController { return; } final long eventTime = SystemClock.uptimeMillis(); - mDeviceMonitors.forEach((deviceId, monitor) -> { - // Re-acquire lock in the lambda to silence error-prone build warnings. - synchronized (mLock) { - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } - } - }); + mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime)); mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS); } } + private void handleMonitorTimeout(int deviceId) { + synchronized (mLock) { + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { + return; + } + final long updateTime = SystemClock.uptimeMillis(); + monitor.onTimeout(updateTime); + } + } + /** Gets the current battery state of an input device. */ public IInputDeviceBatteryState getBatteryState(int deviceId) { synchronized (mLock) { @@ -329,15 +356,11 @@ final class BatteryController { final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); if (monitor == null) { // The input device's battery is not being monitored by any listener. - return queryBatteryStateFromNative(deviceId, updateTime); + return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId)); } // Force the battery state to update, and notify listeners if necessary. - final boolean stateChanged = monitor.updateBatteryState(updateTime); - final State state = monitor.getBatteryStateForReporting(); - if (stateChanged) { - notifyAllListenersForDeviceLocked(state); - } - return state; + monitor.onPoll(updateTime); + return monitor.getBatteryStateForReporting(); } } @@ -379,7 +402,14 @@ final class BatteryController { private final InputManager.InputDeviceListener mInputDeviceListener = new InputManager.InputDeviceListener() { @Override - public void onInputDeviceAdded(int deviceId) {} + public void onInputDeviceAdded(int deviceId) { + synchronized (mLock) { + if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) { + // Start monitoring USI device immediately. + mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId)); + } + } + } @Override public void onInputDeviceRemoved(int deviceId) {} @@ -392,9 +422,7 @@ final class BatteryController { return; } final long eventTime = SystemClock.uptimeMillis(); - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } + monitor.onConfiguration(eventTime); } } }; @@ -422,8 +450,7 @@ final class BatteryController { } // Queries the battery state of an input device from native code. - private State queryBatteryStateFromNative(int deviceId, long updateTime) { - final boolean isPresent = hasBattery(deviceId); + private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) { return new State( deviceId, updateTime, @@ -434,8 +461,9 @@ final class BatteryController { // Holds the state of an InputDevice for which battery changes are currently being monitored. private class DeviceMonitor { - @NonNull - private State mState; + protected final State mState; + // Represents whether the input device has a sysfs battery node. + protected boolean mHasBattery = false; @Nullable private UEventBatteryListener mUEventBatteryListener; @@ -445,26 +473,32 @@ final class BatteryController { // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); - updateBatteryState(eventTime); + configureDeviceMonitor(eventTime); } - // Returns true if the battery state changed since the last time it was updated. - public boolean updateBatteryState(long updateTime) { - mState.updateTime = updateTime; - - final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime); - if (mState.equals(updatedState)) { - return false; + protected void processChangesAndNotify(long eventTime, Consumer<Long> changes) { + final State oldState = getBatteryStateForReporting(); + changes.accept(eventTime); + final State newState = getBatteryStateForReporting(); + if (!oldState.equals(newState)) { + notifyAllListenersForDevice(newState); } - if (mState.isPresent != updatedState.isPresent) { - if (updatedState.isPresent) { + } + + public void onConfiguration(long eventTime) { + processChangesAndNotify(eventTime, this::configureDeviceMonitor); + } + + private void configureDeviceMonitor(long eventTime) { + if (mHasBattery != hasBattery(mState.deviceId)) { + mHasBattery = !mHasBattery; + if (mHasBattery) { startMonitoring(); } else { stopMonitoring(); } + updateBatteryStateFromNative(eventTime); } - mState = updatedState; - return true; } private void startMonitoring() { @@ -483,19 +517,46 @@ final class BatteryController { mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath)); } - private String formatDevPath(String path) { + private String formatDevPath(@NonNull String path) { // Remove the "/sys" prefix if it has one. return path.startsWith("/sys") ? path.substring(4) : path; } - // This must be called when the device is no longer being monitored. - public void stopMonitoring() { + private void stopMonitoring() { if (mUEventBatteryListener != null) { mUEventManager.removeListener(mUEventBatteryListener); mUEventBatteryListener = null; } } + // This must be called when the device is no longer being monitored. + public void onMonitorDestroy() { + stopMonitoring(); + } + + protected void updateBatteryStateFromNative(long eventTime) { + mState.updateIfChanged( + queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); + } + + public void onPoll(long eventTime) { + processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); + } + + public void onUEvent(long eventTime) { + processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); + } + + public boolean requiresPolling() { + return true; + } + + public boolean isPersistent() { + return false; + } + + public void onTimeout(long eventTime) {} + // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { return new State(mState); @@ -503,8 +564,98 @@ final class BatteryController { @Override public String toString() { - return "state=" + mState - + ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none"); + return "DeviceId=" + mState.deviceId + + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + + ", NativeBattery=" + mState + + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none"); + } + } + + // Battery monitoring logic that is specific to stylus devices that support the + // Universal Stylus Initiative (USI) protocol. + private class UsiDeviceMonitor extends DeviceMonitor { + + // For USI devices, we only treat the battery state as valid for a fixed amount of time + // after receiving a battery update. Once the timeout has passed, we signal to all listeners + // that there is no longer a battery present for the device. The battery state is valid + // as long as this callback is non-null. + @Nullable + private Runnable mValidityTimeoutCallback; + + UsiDeviceMonitor(int deviceId) { + super(deviceId); + } + + @Override + public void onPoll(long eventTime) { + // Disregard polling for USI devices. + } + + @Override + public void onUEvent(long eventTime) { + processChangesAndNotify(eventTime, (time) -> { + updateBatteryStateFromNative(time); + markUsiBatteryValid(); + }); + } + + @Override + public void onTimeout(long eventTime) { + processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid()); + } + + @Override + public void onConfiguration(long eventTime) { + super.onConfiguration(eventTime); + + if (!mHasBattery) { + throw new IllegalStateException( + "UsiDeviceMonitor: USI devices are always expected to " + + "report a valid battery, but no battery was detected!"); + } + } + + private void markUsiBatteryValid() { + if (mValidityTimeoutCallback != null) { + mHandler.removeCallbacks(mValidityTimeoutCallback); + } else { + final int deviceId = mState.deviceId; + mValidityTimeoutCallback = + () -> BatteryController.this.handleMonitorTimeout(deviceId); + } + mHandler.postDelayed(mValidityTimeoutCallback, USI_BATTERY_VALIDITY_DURATION_MILLIS); + } + + private void markUsiBatteryInvalid() { + if (mValidityTimeoutCallback == null) { + return; + } + mHandler.removeCallbacks(mValidityTimeoutCallback); + mValidityTimeoutCallback = null; + } + + @Override + public State getBatteryStateForReporting() { + return mValidityTimeoutCallback != null + ? new State(mState) : new State(mState.deviceId); + } + + @Override + public boolean requiresPolling() { + // Do not poll the battery state for USI devices. + return false; + } + + @Override + public boolean isPersistent() { + // Do not remove the battery monitor for USI devices. + return true; + } + + @Override + public String toString() { + return super.toString() + + ", UsiStateIsValid=" + (mValidityTimeoutCallback != null); } } @@ -548,18 +699,33 @@ final class BatteryController { private static class State extends IInputDeviceBatteryState { State(int deviceId) { - initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, - Float.NaN /*capacity*/); + reset(deviceId); } State(IInputDeviceBatteryState s) { - initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); + copyFrom(s); } State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { initialize(deviceId, updateTime, isPresent, status, capacity); } + // Updates this from other if there is a difference between them, ignoring the updateTime. + public void updateIfChanged(IInputDeviceBatteryState other) { + if (!equalsIgnoringUpdateTime(other)) { + copyFrom(other); + } + } + + public void reset(int deviceId) { + initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, + Float.NaN /*capacity*/); + } + + private void copyFrom(IInputDeviceBatteryState s) { + initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); + } + private void initialize(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { this.deviceId = deviceId; @@ -569,11 +735,34 @@ final class BatteryController { this.capacity = capacity; } + private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { + long updateTime = this.updateTime; + this.updateTime = other.updateTime; + boolean eq = this.equals(other); + this.updateTime = updateTime; + return eq; + } + @Override public String toString() { - return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime - + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity - + " }"; + if (!isPresent) { + return "State{<not present>}"; + } + return "State{time=" + updateTime + + ", isPresent=" + isPresent + + ", status=" + status + + ", capacity=" + capacity + + "}"; } } + + // Check if any value in an ArrayMap matches the predicate in an optimized way. + private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) { + for (int i = 0; i < arrayMap.size(); i++) { + if (test.test(arrayMap.valueAt(i))) { + return true; + } + } + return false; + } } diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index a4e295b4f7df..bf00a33d7d20 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -203,6 +203,12 @@ public interface Computer extends PackageDataSnapshot { boolean filterSharedLibPackage(@Nullable PackageStateInternal ps, int uid, int userId, long flags); boolean isCallerSameApp(String packageName, int uid); + /** + * Returns true if the package name and the uid represent the same app. + * + * @param resolveIsolatedUid if true, resolves an isolated uid into the real uid. + */ + boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid); boolean isComponentVisibleToInstantApp(@Nullable ComponentName component); boolean isComponentVisibleToInstantApp(@Nullable ComponentName component, @PackageManager.ComponentType int type); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 5d479d52d6cc..86b8272dbe00 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2209,11 +2209,19 @@ public class ComputerEngine implements Computer { } public final boolean isCallerSameApp(String packageName, int uid) { + return isCallerSameApp(packageName, uid, false /* resolveIsolatedUid */); + } + + @Override + public final boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid) { if (Process.isSdkSandboxUid(uid)) { return (packageName != null && packageName.equals(mService.getSdkSandboxPackageName())); } AndroidPackage pkg = mPackages.get(packageName); + if (resolveIsolatedUid && Process.isIsolated(uid)) { + uid = getIsolatedOwner(uid); + } return pkg != null && UserHandle.getAppId(uid) == pkg.getUid(); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 8fed153825db..6e54d0bbd656 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5242,25 +5242,30 @@ public class PackageManagerService implements PackageSender, TestUtilityService Map<String, String> classLoaderContextMap, String loaderIsa) { int callingUid = Binder.getCallingUid(); - if (PackageManagerService.PLATFORM_PACKAGE_NAME.equals(loadingPackageName) - && callingUid != Process.SYSTEM_UID) { + + // TODO(b/254043366): System server should not report its own dex load because there's + // nothing ART can do with it. + + Computer snapshot = snapshot(); + + // System server should be able to report dex load on behalf of other apps. E.g., it + // could potentially resend the notifications in order to migrate the existing dex load + // info to ART Service. + if (!PackageManagerServiceUtils.isSystemOrRoot() + && !snapshot.isCallerSameApp( + loadingPackageName, callingUid, true /* resolveIsolatedUid */)) { Slog.w(PackageManagerService.TAG, - "Non System Server process reporting dex loads as system server. uid=" - + callingUid); - // Do not record dex loads from processes pretending to be system server. - // Only the system server should be assigned the package "android", so reject calls - // that don't satisfy the constraint. - // - // notifyDexLoad is a PM API callable from the app process. So in theory, apps could - // craft calls to this API and pretend to be system server. Doing so poses no - // particular danger for dex load reporting or later dexopt, however it is a - // sensible check to do in order to verify the expectations. + TextUtils.formatSimple( + "Invalid dex load report. loadingPackageName=%s, uid=%d", + loadingPackageName, callingUid)); return; } + // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`. + int userId = UserHandle.getCallingUserId(); - ApplicationInfo ai = snapshot().getApplicationInfo(loadingPackageName, /*flags*/ 0, - userId); + ApplicationInfo ai = + snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId); if (ai == null) { Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package=" + loadingPackageName + ", user=" + userId); diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index 47a3705388b6..415ddd396ad9 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -707,7 +707,7 @@ final class VerifyingSession { private List<ComponentName> matchVerifiers(PackageInfoLite pkgInfo, List<ResolveInfo> receivers, final PackageVerificationState verificationState) { - if (pkgInfo.verifiers.length == 0) { + if (pkgInfo.verifiers == null || pkgInfo.verifiers.length == 0) { return null; } 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 c81a3eeab965..799ef41f3067 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -649,8 +649,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt Permission bp = mRegistry.getPermission(info.name); added = bp == null; int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel); + enforcePermissionCapLocked(info, tree); if (added) { - enforcePermissionCapLocked(info, tree); bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC); } else if (!bp.isDynamic()) { throw new SecurityException("Not allowed to modify non-dynamic permission " @@ -2156,6 +2156,46 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } /** + * If the package was below api 23, got the SYSTEM_ALERT_WINDOW permission automatically, and + * then updated past api 23, and the app does not satisfy any of the other SAW permission flags, + * the permission should be revoked. + * + * @param newPackage The new package that was installed + * @param oldPackage The old package that was updated + */ + private void revokeSystemAlertWindowIfUpgradedPast23( + @NonNull AndroidPackage newPackage, + @NonNull AndroidPackage oldPackage) { + if (oldPackage.getTargetSdkVersion() >= Build.VERSION_CODES.M + || newPackage.getTargetSdkVersion() < Build.VERSION_CODES.M + || !newPackage.getRequestedPermissions() + .contains(Manifest.permission.SYSTEM_ALERT_WINDOW)) { + return; + } + + Permission saw; + synchronized (mLock) { + saw = mRegistry.getPermission(Manifest.permission.SYSTEM_ALERT_WINDOW); + } + final PackageStateInternal ps = + mPackageManagerInt.getPackageStateInternal(newPackage.getPackageName()); + if (shouldGrantPermissionByProtectionFlags(newPackage, ps, saw, new ArraySet<>()) + || shouldGrantPermissionBySignature(newPackage, saw)) { + return; + } + for (int userId : getAllUserIds()) { + try { + revokePermissionFromPackageForUser(newPackage.getPackageName(), + Manifest.permission.SYSTEM_ALERT_WINDOW, false, userId, + mDefaultPermissionCallback); + } catch (IllegalStateException | SecurityException e) { + Log.e(TAG, "unable to revoke SYSTEM_ALERT_WINDOW for " + + newPackage.getPackageName() + " user " + userId, e); + } + } + } + + /** * We might auto-grant permissions if any permission of the group is already granted. Hence if * the group of a granted permission changes we need to revoke it to avoid having permissions of * the new group auto-granted. @@ -4691,6 +4731,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt if (hasOldPkg) { revokeRuntimePermissionsIfGroupChangedInternal(pkg, oldPkg); revokeStoragePermissionsIfScopeExpandedInternal(pkg, oldPkg); + revokeSystemAlertWindowIfUpgradedPast23(pkg, oldPkg); } if (hasPermissionDefinitionChanges) { revokeRuntimePermissionsIfPermissionDefinitionChangedInternal( diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index ae998067fa03..a6fac4d60fe7 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -114,6 +114,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Rect; +import android.hardware.SensorPrivacyManager; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.hdmi.HdmiAudioSystemClient; @@ -391,6 +392,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { IStatusBarService mStatusBarService; StatusBarManagerInternal mStatusBarManagerInternal; AudioManagerInternal mAudioManagerInternal; + SensorPrivacyManager mSensorPrivacyManager; DisplayManager mDisplayManager; DisplayManagerInternal mDisplayManagerInternal; boolean mPreloadedRecentApps; @@ -1912,6 +1914,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); mAppOpsManager = mContext.getSystemService(AppOpsManager.class); + mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class); mDisplayManager = mContext.getSystemService(DisplayManager.class); mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mPackageManager = mContext.getPackageManager(); @@ -2999,8 +3002,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { if ((metaState & KeyEvent.META_META_MASK) == 0) { return key_not_consumed; } - // Share the same behavior with KEYCODE_LANGUAGE_SWITCH. - case KeyEvent.KEYCODE_LANGUAGE_SWITCH: if (down && repeatCount == 0) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction); @@ -3081,6 +3082,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { return key_not_consumed; } + private void toggleMicrophoneMuteFromKey() { + if (mSensorPrivacyManager.supportsSensorToggle( + SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, + SensorPrivacyManager.Sensors.MICROPHONE)) { + boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, + SensorPrivacyManager.Sensors.MICROPHONE); + mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE, + !isEnabled); + } + } + /** * TV only: recognizes a remote control gesture for capturing a bug report. */ @@ -4013,11 +4026,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; } + case KeyEvent.KEYCODE_MUTE: + result &= ~ACTION_PASS_TO_USER; + if (down && event.getRepeatCount() == 0) { + toggleMicrophoneMuteFromKey(); + } + break; case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: @@ -4195,7 +4213,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mRequestedOrSleepingDefaultDisplay) { mCameraGestureTriggeredDuringGoingToSleep = true; // Wake device up early to prevent display doing redundant turning off/on stuff. - wakeUpFromPowerKey(event.getDownTime()); + wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, + PowerManager.WAKE_REASON_CAMERA_LAUNCH, + "android.policy:CAMERA_GESTURE_PREVENT_LOCK"); } return true; } @@ -4728,11 +4748,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mDefaultDisplayRotation.updateOrientationListener(); reportScreenStateToVrManager(false); - if (mCameraGestureTriggeredDuringGoingToSleep) { - wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, - PowerManager.WAKE_REASON_CAMERA_LAUNCH, - "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK"); - } } } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 4784723b7735..d8b1120c624d 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -42,8 +42,6 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.SynchronousUserSwitchObserver; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -64,7 +62,6 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.BatterySaverPolicyConfig; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; @@ -127,7 +124,6 @@ import com.android.server.UiThread; import com.android.server.UserspaceRebootLogger; import com.android.server.Watchdog; import com.android.server.am.BatteryStatsService; -import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; import com.android.server.policy.WindowManagerPolicy; @@ -284,17 +280,6 @@ public final class PowerManagerService extends SystemService */ private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L; - /** - * Apps targeting Android U and above need to define - * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for - * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect. - * Note that most applications should use {@link android.R.attr#turnScreenOn} or - * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the - * previous foreground app from being resumed first when the screen turns on. - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L; /** Reason ID for holding display suspend blocker. */ private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display"; @@ -318,7 +303,6 @@ public final class PowerManagerService extends SystemService private final SystemPropertiesWrapper mSystemProperties; private final Clock mClock; private final Injector mInjector; - private final PlatformCompat mPlatformCompat; private AppOpsManager mAppOpsManager; private LightsManager mLightsManager; @@ -1012,11 +996,6 @@ public final class PowerManagerService extends SystemService public void set(String key, String val) { SystemProperties.set(key, val); } - - @Override - public boolean getBoolean(String key, boolean def) { - return SystemProperties.getBoolean(key, def); - } }; } @@ -1053,10 +1032,6 @@ public final class PowerManagerService extends SystemService AppOpsManager createAppOpsManager(Context context) { return context.getSystemService(AppOpsManager.class); } - - PlatformCompat createPlatformCompat(Context context) { - return context.getSystemService(PlatformCompat.class); - } } final Constants mConstants; @@ -1114,8 +1089,6 @@ public final class PowerManagerService extends SystemService mAppOpsManager = injector.createAppOpsManager(mContext); - mPlatformCompat = injector.createPlatformCompat(mContext); - mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener(); // Save brightness values: @@ -1626,28 +1599,14 @@ public final class PowerManagerService extends SystemService } if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName) == AppOpsManager.MODE_ALLOWED) { - if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION, - opPackageName, UserHandle.getUserId(opUid))) { - if (mContext.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON) - == PackageManager.PERMISSION_GRANTED) { - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up from app " + opPackageName); - } - return true; - } - } else { - // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only - // check for appOp for apps targeting lower SDK versions - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up from app with " - + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName); - } + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON) + == PackageManager.PERMISSION_GRANTED) { + Slog.i(TAG, "Allowing device wake-up from app " + opPackageName); return true; } } - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { - Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON"); + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { + Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on"); return true; } Slog.w(TAG, "Not allowing device wake-up for " + opPackageName); diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java index c68f9c63b13b..1acf798eb099 100644 --- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java +++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java @@ -48,19 +48,4 @@ interface SystemPropertiesWrapper { * SELinux. libc will log the underlying reason. */ void set(@NonNull String key, @Nullable String val); - - /** - * Get the value for the given {@code key}, returned as a boolean. - * Values 'n', 'no', '0', 'false' or 'off' are considered false. - * Values 'y', 'yes', '1', 'true' or 'on' are considered true. - * (case sensitive). - * If the key does not exist, or has any other value, then the default - * result is returned. - * - * @param key the key to lookup - * @param def a default value to return - * @return the key parsed as a boolean, or def if the key isn't found or is - * not able to be parsed as a boolean. - */ - boolean getBoolean(@NonNull String key, boolean def); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 7ccf85f34737..d378b11f02bf 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2272,6 +2272,25 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + boolean proto = false; + for (int i = 0; i < args.length; i++) { + if ("--proto".equals(args[i])) { + proto = true; + } + } + if (proto) { + if (mBar == null) return; + try (TransferPipe tp = new TransferPipe()) { + // Sending the command to the remote, which needs to execute async to avoid blocking + // See Binder#dumpAsync() for inspiration + mBar.dumpProto(args, tp.getWriteFd()); + // Times out after 5s + tp.go(fd); + } catch (Throwable t) { + Slog.e(TAG, "Error sending command to IStatusBar", t); + } + return; + } synchronized (mLock) { for (int i = 0; i < mDisplayUiState.size(); i++) { diff --git a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java index ff0529f35057..8a6f92750501 100644 --- a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java +++ b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java @@ -16,10 +16,13 @@ package com.android.server.timezonedetector.location; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED; + import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.infoLog; import android.annotation.NonNull; import android.service.timezone.TimeZoneProviderEvent; +import android.service.timezone.TimeZoneProviderStatus; import com.android.i18n.timezone.ZoneInfoDb; @@ -53,7 +56,12 @@ public class ZoneInfoDbTimeZoneProviderEventPreProcessor // enables immediate failover to a secondary provider, one that might provide valid IDs for // the same location, which should provide better behavior than just ignoring the event. if (hasInvalidZones(event)) { - return TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis()); + TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder( + event.getTimeZoneProviderStatus()) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); + return TimeZoneProviderEvent.createUncertainEvent( + event.getCreationElapsedMillis(), providerStatus); } return event; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 2232aa1be76f..81bb3a1f40bc 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8454,7 +8454,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, mCompatDisplayInsets); // Use current screen layout as source because the size of app is independent to parent. - resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride( + resolvedConfig.screenLayout = computeScreenLayout( getConfiguration().screenLayout, resolvedConfig.screenWidthDp, resolvedConfig.screenHeightDp); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 3c847ce0ed89..739f41f170aa 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2188,8 +2188,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED; } - computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, dw, dh, - mDisplayMetrics.density, outConfig); + computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig); mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId, mDisplayInfo); @@ -2289,8 +2288,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp displayInfo.appHeight = appBounds.height(); final DisplayCutout displayCutout = calculateDisplayCutoutForRotation(rotation); displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout; - computeSizeRangesAndScreenLayout(displayInfo, rotated, dw, dh, - mDisplayMetrics.density, outConfig); + computeSizeRanges(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig); return displayInfo; } @@ -2309,6 +2307,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp outConfig.screenHeightDp = (int) (info.mConfigFrame.height() / density + 0.5f); outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale); outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale); + outConfig.screenLayout = computeScreenLayout( + Configuration.resetScreenLayout(outConfig.screenLayout), + outConfig.screenWidthDp, outConfig.screenHeightDp); final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, dw, dh); @@ -2450,7 +2451,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return curSize; } - private void computeSizeRangesAndScreenLayout(DisplayInfo displayInfo, boolean rotated, + private void computeSizeRanges(DisplayInfo displayInfo, boolean rotated, int dw, int dh, float density, Configuration outConfig) { // We need to determine the smallest width that will occur under normal @@ -2477,31 +2478,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (outConfig == null) { return; } - int sl = Configuration.resetScreenLayout(outConfig.screenLayout); - sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh); - sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw); - sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh); - sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw); outConfig.smallestScreenWidthDp = (int) (displayInfo.smallestNominalAppWidth / density + 0.5f); - outConfig.screenLayout = sl; - } - - private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh) { - // Get the app screen size at this rotation. - final Rect size = mDisplayPolicy.getDecorInsetsInfo(rotation, dw, dh).mNonDecorFrame; - - // Compute the screen layout size class for this rotation. - int longSize = size.width(); - int shortSize = size.height(); - if (longSize < shortSize) { - int tmp = longSize; - longSize = shortSize; - shortSize = tmp; - } - longSize = (int) (longSize / density + 0.5f); - shortSize = (int) (shortSize / density + 0.5f); - return Configuration.reduceScreenLayout(curLayout, longSize, shortSize); } private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh) { diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index 55055390b0ff..449e77fca399 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -55,6 +55,7 @@ import android.view.animation.Transformation; import android.window.ScreenCapture; import com.android.internal.R; +import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; import com.android.server.display.DisplayControl; import com.android.server.wm.SurfaceAnimator.AnimationType; @@ -246,7 +247,7 @@ class ScreenRotationAnimation { HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ScreenRotationAnimation#getMedianBorderLuma"); - mStartLuma = RotationAnimationUtils.getMedianBorderLuma(hardwareBuffer, + mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -489,8 +490,8 @@ class ScreenRotationAnimation { return false; } if (!mStarted) { - mEndLuma = RotationAnimationUtils.getLumaOfSurfaceControl(mDisplayContent.getDisplay(), - mDisplayContent.getWindowingLayer()); + mEndLuma = TransitionAnimation.getBorderLuma(mDisplayContent.getWindowingLayer(), + finalWidth, finalHeight); startAnimation(t, maxAnimationDuration, animationScale, finalWidth, finalHeight, exitAnim, enterAnim); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 885968f619f6..391d081ce995 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -22,7 +22,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED; import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -5787,12 +5786,10 @@ class Task extends TaskFragment { return false; } - // Existing Tasks can be reused if a new root task will be created anyway, or for the - // Dream - because there can only ever be one DreamActivity. + // Existing Tasks can be reused if a new root task will be created anyway. final int windowingMode = getWindowingMode(); final int activityType = getActivityType(); - return DisplayContent.alwaysCreateRootTask(windowingMode, activityType) - || activityType == ACTIVITY_TYPE_DREAM; + return DisplayContent.alwaysCreateRootTask(windowingMode, activityType); } void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index efb630291eb7..230b760ce39f 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2189,7 +2189,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { compatScreenHeightDp = inOutConfig.screenHeightDp; } // Reducing the screen layout starting from its parent config. - inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout, + inOutConfig.screenLayout = computeScreenLayout(parentConfig.screenLayout, compatScreenWidthDp, compatScreenHeightDp); } } @@ -2252,16 +2252,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { } } - /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */ - static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp, - int screenHeightDp) { - sourceScreenLayout = sourceScreenLayout - & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK); - final int longSize = Math.max(screenWidthDp, screenHeightDp); - final int shortSize = Math.min(screenWidthDp, screenHeightDp); - return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize); - } - @Override public int getActivityType() { final int applicationType = super.getActivityType(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 4459d45f60a8..b2c8b7ab98d7 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -83,11 +83,11 @@ import android.window.TransitionInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.inputmethod.InputMethodManagerInternal; -import com.android.server.wm.utils.RotationAnimationUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -2190,7 +2190,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe changeInfo.mSnapshot = snapshotSurface; if (isDisplayRotation) { // This isn't cheap, so only do it for display rotations. - changeInfo.mSnapshotLuma = RotationAnimationUtils.getMedianBorderLuma( + changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma( screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace()); } SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 9763df6b967a..c4c66d8c9c3c 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1607,6 +1607,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return false; } + /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */ + static int computeScreenLayout(int sourceScreenLayout, int screenWidthDp, + int screenHeightDp) { + sourceScreenLayout = sourceScreenLayout + & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK); + final int longSize = Math.max(screenWidthDp, screenHeightDp); + final int shortSize = Math.min(screenWidthDp, screenHeightDp); + return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize); + } + // TODO: Users would have their own window containers under the display container? void switchUser(int userId) { for (int i = mChildren.size() - 1; i >= 0; --i) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index a5cd8a92003c..c9d3dac104de 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8698,11 +8698,12 @@ public class WindowManagerService extends IWindowManager.Stub h.ownerPid = callingPid; if (region == null) { - h.replaceTouchableRegionWithCrop = true; + h.replaceTouchableRegionWithCrop(null); } else { h.touchableRegion.set(region); + h.replaceTouchableRegionWithCrop = false; + h.setTouchableRegionCrop(surface); } - h.setTouchableRegionCrop(null /* use the input surface's bounds */); final SurfaceControl.Transaction t = mTransactionFactory.get(); t.setInputWindowInfo(surface, h); diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index 805559035ef9..7c481f51dfd0 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -448,14 +448,8 @@ class WindowToken extends WindowContainer<WindowState> { if (mFixedRotationTransformState != null) { mFixedRotationTransformState.disassociate(this); } - // TODO(b/233855302): Remove TaskFragment override if the DisplayContent uses the same - // bounds for screenLayout calculation. - final Configuration overrideConfig = new Configuration(config); - overrideConfig.screenLayout = TaskFragment.computeScreenLayoutOverride( - overrideConfig.screenLayout, overrideConfig.screenWidthDp, - overrideConfig.screenHeightDp); mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, - overrideConfig, mDisplayContent.getRotation()); + new Configuration(config), mDisplayContent.getRotation()); mFixedRotationTransformState.mAssociatedTokens.add(this); mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames); onFixedRotationStatePrepared(); diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java index b93b8d866a00..c11a6d02eb18 100644 --- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java +++ b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java @@ -16,24 +16,11 @@ package com.android.server.wm.utils; -import static android.hardware.HardwareBuffer.RGBA_8888; import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; -import android.graphics.Color; -import android.graphics.ColorSpace; import android.graphics.Matrix; -import android.graphics.Point; -import android.graphics.Rect; import android.hardware.HardwareBuffer; -import android.media.Image; -import android.media.ImageReader; -import android.view.Display; import android.view.Surface; -import android.view.SurfaceControl; -import android.window.ScreenCapture; - -import java.nio.ByteBuffer; -import java.util.Arrays; /** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/ @@ -46,89 +33,6 @@ public class RotationAnimationUtils { return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; } - /** - * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the - * luminance at the borders of the bitmap - * @return the average luminance of all the pixels at the borders of the bitmap - */ - public static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) { - // Cannot read content from buffer with protected usage. - if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888 - || hasProtectedContent(hardwareBuffer)) { - return 0; - } - - ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(), - hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1); - ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace); - Image image = ir.acquireLatestImage(); - if (image == null || image.getPlanes().length == 0) { - return 0; - } - - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int width = image.getWidth(); - int height = image.getHeight(); - int pixelStride = plane.getPixelStride(); - int rowStride = plane.getRowStride(); - float[] borderLumas = new float[2 * width + 2 * height]; - - // Grab the top and bottom borders - int l = 0; - for (int x = 0; x < width; x++) { - borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); - } - - // Grab the left and right borders - for (int y = 0; y < height; y++) { - borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); - } - - // Cleanup - ir.close(); - - // Oh, is this too simple and inefficient for you? - // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians - Arrays.sort(borderLumas); - return borderLumas[borderLumas.length / 2]; - } - - private static float getPixelLuminance(ByteBuffer buffer, int x, int y, - int pixelStride, int rowStride) { - int offset = y * rowStride + x * pixelStride; - int pixel = 0; - pixel |= (buffer.get(offset) & 0xff) << 16; // R - pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G - pixel |= (buffer.get(offset + 2) & 0xff); // B - pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A - return Color.valueOf(pixel).luminance(); - } - - /** - * Gets the average border luma by taking a screenshot of the {@param surfaceControl}. - * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace) - */ - public static float getLumaOfSurfaceControl(Display display, SurfaceControl surfaceControl) { - if (surfaceControl == null) { - return 0; - } - - Point size = new Point(); - display.getSize(size); - Rect crop = new Rect(0, 0, size.x, size.y); - ScreenCapture.ScreenshotHardwareBuffer buffer = - ScreenCapture.captureLayers(surfaceControl, crop, 1); - if (buffer == null) { - return 0; - } - - return RotationAnimationUtils.getMedianBorderLuma(buffer.getHardwareBuffer(), - buffer.getColorSpace()); - } - public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) { switch (rotation) { case Surface.ROTATION_0: diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 91f5c6999427..352a257e2915 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -49,7 +49,8 @@ public final class CredentialManagerService extends public CredentialManagerService(@NonNull Context context) { super(context, - new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE), + new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE, + /*isMultiple=*/true), null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index f45f62682f1c..aa19241e77dd 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -17,6 +17,11 @@ package com.android.server.credentials; import android.annotation.NonNull; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.RemoteException; import android.util.Log; import com.android.server.infra.AbstractPerUserSystemService; @@ -24,7 +29,7 @@ import com.android.server.infra.AbstractPerUserSystemService; /** * Per-user implementation of {@link CredentialManagerService} */ -public class CredentialManagerServiceImpl extends +public final class CredentialManagerServiceImpl extends AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> { private static final String TAG = "CredManSysServiceImpl"; @@ -34,6 +39,20 @@ public class CredentialManagerServiceImpl extends super(master, lock, userId); } + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new PackageManager.NameNotFoundException( + "Could not get service for " + serviceComponent); + } + return si; + } + /** * Unimplemented getCredentials */ diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 9e449aedf0d9..b74fedf3ff46 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1792,7 +1792,8 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartStatusBarManagerService"); try { statusBar = new StatusBarManagerService(context); - ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar); + ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false, + DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); } catch (Throwable e) { reportWtf("starting StatusBarManagerService", e); } diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS index 2bb16496e0f0..4dda51f2004f 100644 --- a/services/tests/mockingservicestests/OWNERS +++ b/services/tests/mockingservicestests/OWNERS @@ -1,5 +1,8 @@ include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS + +# Game Platform per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS per-file Game* = file:/GAME_MANAGER_OWNERS +per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index abc32c9339ae..b7e66f23a706 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -291,20 +291,30 @@ public class BroadcastQueueModernImplTest { final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants, PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN)); + // enqueue a bg-priority broadcast then a fg-priority one + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone); + queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, 0); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane); queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0); + // verify that: + // (a) the queue is immediately runnable by existence of a fg-priority broadcast + // (b) the next one up is the fg-priority broadcast despite its later enqueue time queue.setProcessCached(false); assertTrue(queue.isRunnable()); assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt()); assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked()); + assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord); queue.setProcessCached(true); assertTrue(queue.isRunnable()); assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt()); assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked()); + assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index c12544897941..d9a26c68f3ed 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -549,12 +549,6 @@ public class BroadcastQueueTest { receivers, false, null, null, userId); } - private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp, - List<Object> receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) { - return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), - receivers, true, orderedResultTo, orderedExtras, UserHandle.USER_SYSTEM); - } - private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, BroadcastOptions options, List<Object> receivers) { return makeBroadcastRecord(intent, callerApp, options, @@ -562,12 +556,24 @@ public class BroadcastQueueTest { } private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, + List<Object> receivers, IIntentReceiver resultTo) { + return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), + receivers, false, resultTo, null, UserHandle.USER_SYSTEM); + } + + private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp, + List<Object> receivers, IIntentReceiver resultTo, Bundle resultExtras) { + return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), + receivers, true, resultTo, resultExtras, UserHandle.USER_SYSTEM); + } + + private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, BroadcastOptions options, List<Object> receivers, boolean ordered, - IIntentReceiver orderedResultTo, Bundle orderedExtras, int userId) { + IIntentReceiver resultTo, Bundle resultExtras, int userId) { return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null, callerApp.getPid(), callerApp.info.uid, false, null, null, null, null, - AppOpsManager.OP_NONE, options, receivers, callerApp, orderedResultTo, - Activity.RESULT_OK, null, orderedExtras, ordered, false, false, userId, false, null, + AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo, + Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, false, null, false, null); } @@ -1347,6 +1353,26 @@ public class BroadcastQueueTest { } /** + * Verify that we deliver results for unordered broadcasts. + */ + @Test + public void testUnordered_ResultTo() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final IApplicationThread callerThread = callerApp.getThread(); + + final IIntentReceiver resultTo = mock(IIntentReceiver.class); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo)); + + waitForIdle(); + verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)), + eq(Activity.RESULT_OK), any(), any(), eq(false), + anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + } + + /** * Verify that we're not surprised by a process attempting to finishing a * broadcast when none is in progress. */ diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index eb1314194aa3..ffacbf331d89 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -16,7 +16,7 @@ package com.android.server.biometrics.sensors; -import static android.testing.TestableLooper.RunWithLooper; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -24,8 +24,10 @@ import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -35,6 +37,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.IBiometricService; import android.os.Binder; @@ -63,28 +66,26 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.function.Supplier; @Presubmit @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper(setAsMainLooper = true) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class BiometricSchedulerTest { private static final String TAG = "BiometricSchedulerTest"; private static final int TEST_SENSOR_ID = 1; private static final int LOG_NUM_RECENT_OPERATIONS = 2; - + @Rule + public final TestableContext mContext = + new TestableContext(InstrumentationRegistry.getContext(), null); private BiometricScheduler mScheduler; private IBinder mToken; - @Mock private IBiometricService mBiometricService; - @Rule - public final TestableContext mContext = - new TestableContext(InstrumentationRegistry.getContext(), null); - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -323,7 +324,7 @@ public class BiometricSchedulerTest { client1.getCallback().onClientFinished(client1, true /* success */); waitForIdle(); verify(callback).onError(anyInt(), anyInt(), - eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), + eq(BIOMETRIC_ERROR_CANCELED), eq(0) /* vendorCode */); assertNull(mScheduler.getCurrentClient()); assertTrue(client1.isAlreadyDone()); @@ -484,7 +485,7 @@ public class BiometricSchedulerTest { mScheduler.scheduleClientMonitor(interrupter); waitForIdle(); - verify((Interruptable) interruptableMonitor).cancel(); + verify(interruptableMonitor).cancel(); mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */); } @@ -500,7 +501,7 @@ public class BiometricSchedulerTest { mScheduler.scheduleClientMonitor(interrupter); waitForIdle(); - verify((Interruptable) interruptableMonitor, never()).cancel(); + verify(interruptableMonitor, never()).cancel(); } @Test @@ -514,21 +515,180 @@ public class BiometricSchedulerTest { assertTrue(client.mDestroyed); } + @Test + public void testClearBiometricQueue_clearsHungAuthOperation() { + // Creating a hung client + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + waitForIdle(); + + mScheduler.startWatchdog(); + waitForIdle(); + + //Checking client is hung + verify(callback1).onClientStarted(client1); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + assertNotNull(mScheduler.mCurrentOperation); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + looper.moveTimeForward(10000); + waitForIdle(); + looper.moveTimeForward(3000); + waitForIdle(); + + // The hung client did not honor this operation, verify onError and authenticated + // were never called. + assertFalse(client1.mOnErrorCalled); + assertFalse(client1.mAuthenticateCalled); + verify(callback1).onClientFinished(client1, false /* success */); + assertNull(mScheduler.mCurrentOperation); + assertEquals(0, mScheduler.getCurrentPendingCount()); + } + + @Test + public void testAuthWorks_afterClearBiometricQueue() { + // Creating a hung client + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + + assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + //Checking client is hung + waitForIdle(); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + + // The watchdog should kick off the cancellation + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + // The hung client did not honor this operation, verify onError and authenticated + // were never called. + assertFalse(client1.mOnErrorCalled); + assertFalse(client1.mAuthenticateCalled); + verify(callback1).onClientFinished(client1, false /* success */); + assertEquals(0, mScheduler.getCurrentPendingCount()); + assertNull(mScheduler.mCurrentOperation); + + + //Run additional auth client + final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client2, callback2); + + assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + waitForIdle(); + + //Ensure auth client passes + verify(callback2).onClientStarted(client2); + client2.getCallback().onClientFinished(client2, true); + waitForIdle(); + + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + //Asserting auth client passes + assertTrue(client2.isAlreadyDone()); + assertNotNull(mScheduler.mCurrentOperation); + } + + @Test + public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() { + //Creating clients + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + waitForIdle(); + + assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(2, mScheduler.getCurrentPendingCount()); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + verify(callback1).onClientStarted(client1); + + //Client finishes successfully + client1.getCallback().onClientFinished(client1, true); + waitForIdle(); + + // The watchdog should kick off the cancellation + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + //Watchdog does not clear pending operations + assertEquals(1, mScheduler.getCurrentPendingCount()); + assertNotNull(mScheduler.mCurrentOperation); + + } + private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception { return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer)); } + private void waitForIdle() { + TestableLooper.get(this).processAllMessages(); + } + private static class TestAuthenticationClient extends AuthenticationClient<Object> { boolean mStartedHal = false; boolean mStoppedHal = false; boolean mDestroyed = false; int mNumCancels = 0; + boolean mAuthenticateCalled = false; + boolean mOnErrorCalled = false; - public TestAuthenticationClient(@NonNull Context context, + TestAuthenticationClient(@NonNull Context context, @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener) { + this(context, lazyDaemon, token, listener, 1 /* cookie */); + } + + TestAuthenticationClient(@NonNull Context context, + @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token, + @NonNull ClientMonitorCallbackConverter listener, int cookie) { super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */, - false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */, + false /* restricted */, TAG, cookie, false /* requireConfirmation */, TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class), true /* isStrongBiometric */, null /* taskStackListener */, mock(LockoutTracker.class), false /* isKeyguard */, @@ -546,7 +706,19 @@ public class BiometricSchedulerTest { } @Override - protected void handleLifecycleAfterAuth(boolean authenticated) {} + protected void handleLifecycleAfterAuth(boolean authenticated) { + } + + @Override + public void onAuthenticated(BiometricAuthenticator.Identifier identifier, + boolean authenticated, ArrayList<Byte> hardwareAuthToken) { + mAuthenticateCalled = true; + } + + @Override + protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) { + mOnErrorCalled = true; + } @Override public boolean wasUserDetected() { @@ -651,8 +823,4 @@ public class BiometricSchedulerTest { mDestroyed = true; } } - - private void waitForIdle() { - TestableLooper.get(this).processAllMessages(); - } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index c5ba360b6170..02bbe658f9b2 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -587,9 +587,9 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int keyCode = KeyEvent.KEYCODE_A; final int action = VirtualKeyEvent.ACTION_UP; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 1, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */1, /* displayId= */ 1, PHYS, + DEVICE_ID); + mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode) .setAction(action).build()); verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action); @@ -612,9 +612,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK; final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder() .setButtonCode(buttonCode) @@ -627,9 +626,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK; final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -653,9 +651,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = -0.2f; final float y = 0.7f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder() .setRelativeX(x).setRelativeY(y).build()); @@ -667,9 +664,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = -0.2f; final float y = 0.7f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -694,9 +690,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = 0.5f; final float y = 1f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder() .setXAxisMovement(x) @@ -709,9 +704,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = 0.5f; final float y = 1f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -742,9 +736,8 @@ public class VirtualDeviceManagerServiceTest { final float x = 100.5f; final float y = 200.5f; final int action = VirtualTouchEvent.ACTION_UP; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS, + DEVICE_ID); mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build()); verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN, @@ -761,9 +754,8 @@ public class VirtualDeviceManagerServiceTest { final int action = VirtualTouchEvent.ACTION_UP; final float pressure = 1.0f; final float majorAxisSize = 10.0f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3, - /* displayId= */ 1, PHYS, DEVICE_ID)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS, + DEVICE_ID); mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType) .setPressure(pressure).setMajorAxisSize(majorAxisSize).build()); diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java index 6860abf40b56..062bde8f080b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX; @@ -660,6 +661,117 @@ public class DisplayManagerServiceTest { firstDisplayId); } + /** Tests that the virtual device is created in a device display group. */ + @Test + public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_DENIED); + + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) + .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + + // Create a first virtual display. A display group should be created for this display on the + // virtual device. + final VirtualDisplayConfig.Builder builder1 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId1 = + localService.createVirtualDisplay( + builder1.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + + // Create a second virtual display. This should be added to the previously created display + // group. + final VirtualDisplayConfig.Builder builder2 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId2 = + localService.createVirtualDisplay( + builder2.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; + + assertEquals( + "Both displays should be added to the same displayGroup.", + displayGroupId1, + displayGroupId2); + } + + /** + * Tests that the virtual display is not added to the device display group when + * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set. + */ + @Test + public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_DENIED); + + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) + .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + + // Create a first virtual display. A display group should be created for this display on the + // virtual device. + final VirtualDisplayConfig.Builder builder1 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId1 = + localService.createVirtualDisplay( + builder1.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + + // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP, + // the display should not be added to the previously created display group. + final VirtualDisplayConfig.Builder builder2 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId2 = + localService.createVirtualDisplay( + builder2.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; + + assertNotEquals( + "Display 1 should be in the device display group and display 2 in its own display" + + " group.", + displayGroupId1, + displayGroupId2); + } + @Test public void testGetDisplayIdToMirror() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java index 0b33c30fd7e8..657bda633ab5 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -369,6 +369,98 @@ public class LogicalDisplayMapperTest { } @Test + public void testDevicesAreAddedToDeviceDisplayGroups() { + // Create the default internal display of the device. + LogicalDisplay defaultDisplay = + add( + createDisplayDevice( + Display.TYPE_INTERNAL, + 600, + 800, + DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); + + // Create 3 virtual displays associated with a first virtual device. + int deviceId1 = 1; + TestDisplayDevice display1 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display1", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display1, deviceId1); + LogicalDisplay virtualDevice1Display1 = add(display1); + + TestDisplayDevice display2 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display2", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display2, deviceId1); + LogicalDisplay virtualDevice1Display2 = add(display2); + + TestDisplayDevice display3 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display3", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display3, deviceId1); + LogicalDisplay virtualDevice1Display3 = add(display3); + + // Create another 3 virtual displays associated with a second virtual device. + int deviceId2 = 2; + TestDisplayDevice display4 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display1", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display4, deviceId2); + LogicalDisplay virtualDevice2Display1 = add(display4); + + TestDisplayDevice display5 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display2", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display5, deviceId2); + LogicalDisplay virtualDevice2Display2 = add(display5); + + // The final display is created with FLAG_OWN_DISPLAY_GROUP set. + TestDisplayDevice display6 = + createDisplayDevice( + Display.TYPE_VIRTUAL, + "virtualDevice2Display3", + 600, + 800, + DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display6, deviceId2); + LogicalDisplay virtualDevice2Display3 = add(display6); + + // Verify that the internal display is in the default display group. + assertEquals( + DEFAULT_DISPLAY_GROUP, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(defaultDisplay))); + + // Verify that all the displays for virtual device 1 are in the same (non-default) display + // group. + int virtualDevice1DisplayGroupId = + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display1)); + assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice1DisplayGroupId); + assertEquals( + virtualDevice1DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display2))); + assertEquals( + virtualDevice1DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display3))); + + // The first 2 displays for virtual device 2 should be in the same non-default group. + int virtualDevice2DisplayGroupId = + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display1)); + assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice2DisplayGroupId); + assertEquals( + virtualDevice2DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display2))); + // virtualDevice2Display3 was created with FLAG_OWN_DISPLAY_GROUP and shouldn't be grouped + // with other displays of this device or be in the default display group. + assertNotEquals( + virtualDevice2DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display3))); + assertNotEquals( + DEFAULT_DISPLAY_GROUP, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display3))); + } + + @Test public void testDeviceShouldBeWoken() { assertTrue(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN, DEVICE_STATE_CLOSED, @@ -416,14 +508,22 @@ public class LogicalDisplayMapperTest { ///////////////// private TestDisplayDevice createDisplayDevice(int type, int width, int height, int flags) { - return createDisplayDevice(new TestUtils.TestDisplayAddress(), type, width, height, flags); + return createDisplayDevice( + new TestUtils.TestDisplayAddress(), /* uniqueId */ "", type, width, height, flags); + } + + private TestDisplayDevice createDisplayDevice( + int type, String uniqueId, int width, int height, int flags) { + return createDisplayDevice( + new TestUtils.TestDisplayAddress(), uniqueId, type, width, height, flags); } private TestDisplayDevice createDisplayDevice( - DisplayAddress address, int type, int width, int height, int flags) { + DisplayAddress address, String uniqueId, int type, int width, int height, int flags) { TestDisplayDevice device = new TestDisplayDevice(); DisplayDeviceInfo displayDeviceInfo = device.getSourceInfo(); displayDeviceInfo.type = type; + displayDeviceInfo.uniqueId = uniqueId; displayDeviceInfo.width = width; displayDeviceInfo.height = height; displayDeviceInfo.flags = flags; diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java new file mode 100644 index 000000000000..303a370b0ba9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.dreams; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.service.dreams.IDreamService; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DreamControllerTest { + @Mock + private DreamController.Listener mListener; + @Mock + private Context mContext; + @Mock + private IBinder mIBinder; + @Mock + private IDreamService mIDreamService; + + @Captor + private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; + @Captor + private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; + + private final TestLooper mLooper = new TestLooper(); + private final Handler mHandler = new Handler(mLooper.getLooper()); + + private DreamController mDreamController; + + private Binder mToken; + private ComponentName mDreamName; + private ComponentName mOverlayName; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mIDreamService.asBinder()).thenReturn(mIBinder); + when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService); + when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); + + mToken = new Binder(); + mDreamName = ComponentName.unflattenFromString("dream"); + mOverlayName = ComponentName.unflattenFromString("dream_overlay"); + mDreamController = new DreamController(mContext, mHandler, mListener); + } + + @Test + public void startDream_attachOnServiceConnected() throws RemoteException { + // Call dream controller to start dreaming. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + + // Mock service connected. + final ServiceConnection serviceConnection = captureServiceConnection(); + serviceConnection.onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Verify that dream service is called to attach. + verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any()); + } + + @Test + public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted() + throws RemoteException { + // Start first dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + clearInvocations(mContext); + + // Set up second dream. + final Binder newToken = new Binder(); + final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream"); + final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay"); + final IDreamService newDreamService = mock(IDreamService.class); + final IBinder newBinder = mock(IBinder.class); + when(newDreamService.asBinder()).thenReturn(newBinder); + when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService); + + // Start second dream. + mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(newDreamName, newBinder); + mLooper.dispatchAll(); + + // Mock second dream started. + verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/, + mRemoteCallbackCaptor.capture()); + mRemoteCallbackCaptor.getValue().sendResult(null /*data*/); + mLooper.dispatchAll(); + + // Verify that the first dream is called to detach. + verify(mIDreamService).detach(); + } + + @Test + public void stopDream_detachFromService() throws RemoteException { + // Start dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Stop dream. + mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/); + + // Verify that dream service is called to detach. + verify(mIDreamService).detach(); + } + + private ServiceConnection captureServiceConnection() { + verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), + any()); + return mServiceConnectionACaptor.getValue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java index 545f3183ee26..3a57db9b975c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java @@ -19,7 +19,6 @@ package com.android.server.hdmi; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.Constants.ADDR_TV; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -47,7 +46,6 @@ import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; import java.util.Collections; /** Tests for {@link DevicePowerStatusAction} */ @@ -65,7 +63,6 @@ public class DevicePowerStatusActionTest { private FakePowerManagerWrapper mPowerManager; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mPhysicalAddress; private DevicePowerStatusAction mDevicePowerStatusAction; @@ -79,7 +76,8 @@ public class DevicePowerStatusActionTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); - mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlService = new HdmiControlService(mContextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), new FakeAudioDeviceVolumeManagerWrapper()) { @Override AudioManager getAudioManager() { @@ -117,11 +115,8 @@ public class DevicePowerStatusActionTest { mHdmiControlService.setPowerManager(mPowerManager); mPhysicalAddress = 0x2000; mNativeWrapper.setPhysicalAddress(mPhysicalAddress); - mPlaybackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - mPlaybackDevice.init(); - mLocalDevices.add(mPlaybackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mTestLooper.dispatchAll(); + mPlaybackDevice = mHdmiControlService.playback(); mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV, mCallbackMock); mTestLooper.dispatchAll(); @@ -213,7 +208,6 @@ public class DevicePowerStatusActionTest { mHdmiControlService.getHdmiCecConfig().setIntValue( HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); mTestLooper.dispatchAll(); @@ -238,7 +232,6 @@ public class DevicePowerStatusActionTest { mHdmiControlService.getHdmiCecConfig().setIntValue( HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV); mNativeWrapper.onCecMessage(reportPhysicalAddress); @@ -263,7 +256,6 @@ public class DevicePowerStatusActionTest { mHdmiControlService.getHdmiCecConfig().setIntValue( HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV); mNativeWrapper.onCecMessage(reportPhysicalAddress); @@ -293,6 +285,12 @@ public class DevicePowerStatusActionTest { @Test public void pendingActionDoesNotBlockSendingStandby() throws Exception { + HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource( + mPlaybackDevice.getDeviceInfo().getLogicalAddress(), + mPhysicalAddress); + assertThat(mPlaybackDevice.handleActiveSource(message)) + .isEqualTo(Constants.HANDLED); + mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction); mTestLooper.dispatchAll(); mNativeWrapper.clearResultMessages(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java index eb7a76182054..7df007813b34 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java @@ -27,7 +27,6 @@ import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3; import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE; import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_DEVICE_POWER_ON; import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_REPORT_POWER_STATUS; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -86,7 +85,6 @@ public class DeviceSelectActionFromPlaybackTest { private FakePowerManagerWrapper mPowerManager; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mPlaybackLogicalAddress1; private int mPlaybackLogicalAddress2; @@ -101,7 +99,8 @@ public class DeviceSelectActionFromPlaybackTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), + new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -119,8 +118,6 @@ public class DeviceSelectActionFromPlaybackTest { }; - mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService); - mHdmiCecLocalDevicePlayback.init(); mHdmiControlService.setIoLooper(mMyLooper); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); mNativeWrapper = new FakeNativeWrapper(); @@ -135,16 +132,14 @@ public class DeviceSelectActionFromPlaybackTest { mHdmiCecController, mHdmiMhlControllerStub); mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork); - mLocalDevices.add(mHdmiCecLocalDevicePlayback); mHdmiControlService.initService(); mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mNativeWrapper.setPhysicalAddress(0x0000); mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); mTestLooper.dispatchAll(); mNativeWrapper.clearResultMessages(); - + mHdmiCecLocalDevicePlayback = mHdmiControlService.playback(); // The addresses depend on local device's LA. // This help the tests to pass with every local device LA. mPlaybackLogicalAddress1 = diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java index 72d36b00d73d..ac57834ed5b0 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java @@ -26,7 +26,6 @@ import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_DEVICE_POWER_ON; import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_REPORT_POWER_STATUS; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -101,7 +100,6 @@ public class DeviceSelectActionFromTvTest { private FakePowerManagerWrapper mPowerManager; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); @Before public void setUp() { @@ -110,7 +108,8 @@ public class DeviceSelectActionFromTvTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), + new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -127,8 +126,7 @@ public class DeviceSelectActionFromTvTest { } }; - mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); - mHdmiCecLocalDeviceTv.init(); + mHdmiControlService.setIoLooper(mMyLooper); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); mNativeWrapper = new FakeNativeWrapper(); @@ -136,7 +134,6 @@ public class DeviceSelectActionFromTvTest { mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(mHdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDeviceTv); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_PLAYBACK_1, @@ -149,12 +146,12 @@ public class DeviceSelectActionFromTvTest { mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mNativeWrapper.setPhysicalAddress(0x0000); mTestLooper.dispatchAll(); mNativeWrapper.clearResultMessages(); mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_1); mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_2); + mHdmiCecLocalDeviceTv = mHdmiControlService.tv(); } private static class TestActionTimer implements ActionTimer { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java index 9f744f9373ed..d2fe6da88830 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java @@ -19,7 +19,6 @@ import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.Constants.PATH_RELATIONSHIP_ANCESTOR; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -35,6 +34,7 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.tv.cec.V1_0.SendMessageResult; import android.os.Binder; @@ -55,7 +55,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; -import java.util.ArrayList; import java.util.Collections; /** @@ -68,7 +67,6 @@ public class HdmiCecAtomLoggingTest { private HdmiCecAtomWriter mHdmiCecAtomWriterSpy; private HdmiControlService mHdmiControlServiceSpy; private HdmiCecController mHdmiCecController; - private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback; private HdmiMhlControllerStub mHdmiMhlControllerStub; private FakeNativeWrapper mNativeWrapper; private FakePowerManagerWrapper mPowerManager; @@ -77,7 +75,6 @@ public class HdmiCecAtomLoggingTest { private Context mContextSpy; private TestLooper mTestLooper = new TestLooper(); private int mPhysicalAddress = 0x1110; - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private HdmiPortInfo[] mHdmiPortInfo; @Before @@ -89,7 +86,8 @@ public class HdmiCecAtomLoggingTest { mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); - mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), new FakeAudioDeviceVolumeManagerWrapper())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); @@ -123,14 +121,9 @@ public class HdmiCecAtomLoggingTest { mNativeWrapper.setPortInfo(hdmiPortInfos); mNativeWrapper.setPortConnectionStatus(1, true); - mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy); - mHdmiCecLocalDevicePlayback.init(); - mLocalDevices.add(mHdmiCecLocalDevicePlayback); - mHdmiControlServiceSpy.initService(); mPowerManager = new FakePowerManagerWrapper(mContextSpy); mHdmiControlServiceSpy.setPowerManager(mPowerManager); - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index 91d265c81083..08d0e9053e2b 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -48,7 +48,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.ArrayList; -import java.util.Collections; @SmallTest @Presubmit @@ -80,15 +79,19 @@ public class HdmiCecLocalDeviceAudioSystemTest { private HdmiDeviceInfo mDeviceInfo; private boolean mArcSupport; private HdmiPortInfo[] mHdmiPortInfo; + private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>(); @Before public void setUp() { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); + mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + mLocalDeviceTypes, + new FakeAudioDeviceVolumeManagerWrapper()) { @Override AudioManager getAudioManager() { return new AudioManager() { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index fe9e0b6a2d07..75c4d92ab9f9 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -47,6 +47,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.ArrayList; +import java.util.Collections; import java.util.concurrent.TimeUnit; @SmallTest @@ -78,7 +79,6 @@ public class HdmiCecLocalDevicePlaybackTest { private TestLooper mTestLooper = new TestLooper(); private FakePowerManagerWrapper mPowerManager; private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); - private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>(); private int mPlaybackPhysicalAddress; private int mPlaybackLogicalAddress; private boolean mWokenUp; @@ -91,10 +91,10 @@ public class HdmiCecLocalDevicePlaybackTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); - mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), + new FakeAudioDeviceVolumeManagerWrapper()) { @Override void wakeUp() { @@ -128,8 +128,6 @@ public class HdmiCecLocalDevicePlaybackTest { } }; - mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService); - mHdmiCecLocalDevicePlayback.init(); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); mHdmiControlService.setIoLooper(mMyLooper); mNativeWrapper = new FakeNativeWrapper(); @@ -137,7 +135,6 @@ public class HdmiCecLocalDevicePlaybackTest { mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(mHdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDevicePlayback); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false); @@ -148,10 +145,11 @@ public class HdmiCecLocalDevicePlaybackTest { mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mPlaybackPhysicalAddress = 0x2000; mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress); mTestLooper.dispatchAll(); + mHdmiCecLocalDevicePlayback = mHdmiControlService.playback(); + mLocalDevices.add(mHdmiCecLocalDevicePlayback); mPlaybackLogicalAddress = mHdmiCecLocalDevicePlayback.getDeviceInfo().getLogicalAddress(); mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV); mNativeWrapper.clearResultMessages(); @@ -1108,7 +1106,11 @@ public class HdmiCecLocalDevicePlaybackTest { HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW); mPowerManager.setInteractive(true); - HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress, + mPlaybackPhysicalAddress); + assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message)) + .isEqualTo(Constants.HANDLED); + message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message)) .isEqualTo(Constants.HANDLED); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index 8112ca8fbb14..7a2a5838134c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -128,7 +128,8 @@ public class HdmiCecLocalDeviceTvTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), + new FakeAudioDeviceVolumeManagerWrapper()) { @Override void wakeUp() { mWokenUp = true; @@ -165,8 +166,6 @@ public class HdmiCecLocalDeviceTvTest { } }; - mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); - mHdmiCecLocalDeviceTv.init(); mHdmiControlService.setIoLooper(mMyLooper); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); mNativeWrapper = new FakeNativeWrapper(); @@ -174,7 +173,6 @@ public class HdmiCecLocalDeviceTvTest { mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(mHdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDeviceTv); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false); @@ -185,11 +183,12 @@ public class HdmiCecLocalDeviceTvTest { mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTvPhysicalAddress = 0x0000; mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress); mTestLooper.dispatchAll(); + mHdmiCecLocalDeviceTv = mHdmiControlService.tv(); mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(); + mLocalDevices.add(mHdmiCecLocalDeviceTv); for (String sad : SADS_NOT_TO_QUERY) { mHdmiControlService.getHdmiCecConfig().setIntValue( sad, HdmiControlManager.QUERY_SAD_DISABLED); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java index b94deeddb8af..a08e398b4010 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java @@ -16,7 +16,6 @@ package com.android.server.hdmi; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -25,6 +24,7 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.os.Looper; import android.os.test.TestLooper; @@ -40,7 +40,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.util.ArrayList; import java.util.Collections; @SmallTest @@ -57,7 +56,6 @@ public class HdmiCecPowerStatusControllerTest { private FakeNativeWrapper mNativeWrapper; private FakePowerManagerWrapper mPowerManager; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private HdmiControlService mHdmiControlService; private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback; @@ -66,7 +64,8 @@ public class HdmiCecPowerStatusControllerTest { Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); Looper myLooper = mTestLooper.getLooper(); - mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList(), + mHdmiControlService = new HdmiControlService(contextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { @@ -90,9 +89,6 @@ public class HdmiCecPowerStatusControllerTest { }; mHdmiControlService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); - mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - mHdmiCecLocalDevicePlayback.init(); mHdmiControlService.setIoLooper(myLooper); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(contextSpy)); mNativeWrapper = new FakeNativeWrapper(); @@ -100,7 +96,6 @@ public class HdmiCecPowerStatusControllerTest { mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(hdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDevicePlayback); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false); @@ -111,10 +106,9 @@ public class HdmiCecPowerStatusControllerTest { mPowerManager = new FakePowerManagerWrapper(contextSpy); mHdmiControlService.setPowerManager(mPowerManager); mHdmiControlService.getHdmiCecNetwork().initPortInfo(); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mNativeWrapper.setPhysicalAddress(0x2000); mTestLooper.dispatchAll(); - + mHdmiCecLocalDevicePlayback = mHdmiControlService.playback(); mHdmiCecPowerStatusController = new HdmiCecPowerStatusController(mHdmiControlService); mNativeWrapper.clearResultMessages(); } @@ -254,7 +248,6 @@ public class HdmiCecPowerStatusControllerTest { private void setCecVersion(int version) { mHdmiControlService.getHdmiCecConfig().setIntValue( HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, version); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 674e47168392..1b867be81669 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlStatusChangeListener; @@ -61,7 +62,6 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Optional; /** @@ -84,14 +84,17 @@ public class HdmiControlServiceTest { private TestLooper mTestLooper = new TestLooper(); private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private HdmiPortInfo[] mHdmiPortInfo; + private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>(); @Before public void setUp() throws Exception { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); + mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); + mLocalDeviceTypes.add(DEVICE_AUDIO_SYSTEM); - mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); @@ -228,8 +231,6 @@ public class HdmiControlServiceTest { mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); mNativeWrapper.clearResultMessages(); - - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); assertThat(mHdmiControlServiceSpy.getInitialPowerStatus()).isEqualTo( @@ -461,7 +462,6 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV, @@ -480,7 +480,6 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV, @@ -502,7 +501,6 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); HdmiCecMessage reportFeatures = ReportFeaturesMessage.build( @@ -519,7 +517,6 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); HdmiCecMessage reportFeatures = ReportFeaturesMessage.build( diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java index 1fa3871347f8..9b8cedfa6234 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java @@ -88,7 +88,8 @@ public class OneTouchPlayActionTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); - mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlService = new HdmiControlService(mContextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), new FakeAudioDeviceVolumeManagerWrapper()) { @Override AudioManager getAudioManager() { @@ -142,11 +143,7 @@ public class OneTouchPlayActionTest { public void succeedWithUnknownTvDevice() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.clearResultMessages(); @@ -191,11 +188,7 @@ public class OneTouchPlayActionTest { public void succeedAfterGettingPowerStatusOn_Cec14b() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -244,11 +237,7 @@ public class OneTouchPlayActionTest { public void succeedAfterGettingTransientPowerStatus_Cec14b() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -310,11 +299,7 @@ public class OneTouchPlayActionTest { public void timeOut_Cec14b() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -359,11 +344,7 @@ public class OneTouchPlayActionTest { @Test public void succeedIfPowerStatusOn_Cec20() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -399,11 +380,8 @@ public class OneTouchPlayActionTest { @Test public void succeedIfPowerStatusUnknown_Cec20() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -453,11 +431,7 @@ public class OneTouchPlayActionTest { @Test public void succeedIfPowerStatusStandby_Cec20() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); @@ -510,11 +484,6 @@ public class OneTouchPlayActionTest { assertThat(mHdmiControlService.isAddressAllocated()).isFalse(); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - TestCallback callback = new TestCallback(); mHdmiControlService.oneTouchPlay(callback); @@ -524,9 +493,8 @@ public class OneTouchPlayActionTest { mNativeWrapper.clearResultMessages(); setHdmiControlEnabled(true); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); - mTestLooper.dispatchAll(); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); HdmiCecMessage reportPowerStatusMessage = HdmiCecMessageBuilder.buildReportPowerStatus( @@ -554,12 +522,7 @@ public class OneTouchPlayActionTest { public void succeedWithAddressAllocated_Cec14b() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); assertThat(mHdmiControlService.isAddressAllocated()).isTrue(); @@ -632,11 +595,7 @@ public class OneTouchPlayActionTest { public void noWakeUpOnReportPowerStatus() throws Exception { setUp(true); - HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( - mHdmiControlService); - playbackDevice.init(); - mLocalDevices.add(playbackDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback(); mTestLooper.dispatchAll(); mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java index e5058bea8d1b..f72ac713f5b4 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java @@ -20,7 +20,6 @@ import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -44,7 +43,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -60,7 +58,6 @@ public class PowerStatusMonitorActionTest { private FakePowerManagerWrapper mPowerManager; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mPhysicalAddress; private HdmiCecLocalDeviceTv mTvDevice; @@ -101,9 +98,6 @@ public class PowerStatusMonitorActionTest { this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(hdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mTvDevice = new HdmiCecLocalDeviceTv(mHdmiControlService); - mTvDevice.init(); - mLocalDevices.add(mTvDevice); mTestLooper.dispatchAll(); HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[2]; hdmiPortInfo[0] = @@ -117,8 +111,8 @@ public class PowerStatusMonitorActionTest { mHdmiControlService.setPowerManager(mPowerManager); mPhysicalAddress = 0x0000; mNativeWrapper.setPhysicalAddress(mPhysicalAddress); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); + mTvDevice = mHdmiControlService.tv(); mNativeWrapper.clearResultMessages(); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java index c2519caabfee..c07d4be78047 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java @@ -18,12 +18,12 @@ package com.android.server.hdmi; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -69,7 +69,6 @@ public class RequestSadActionTest { private FakePowerManagerWrapper mPowerManager; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mTvLogicalAddress; private List<byte[]> mSupportedSads; private RequestSadCallback mCallback = @@ -97,7 +96,7 @@ public class RequestSadActionTest { mMyLooper = mTestLooper.getLooper(); mHdmiControlService = - new HdmiControlService(context, Collections.emptyList(), + new HdmiControlService(context, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { @@ -115,8 +114,6 @@ public class RequestSadActionTest { } }; - mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); - mHdmiCecLocalDeviceTv.init(); mHdmiControlService.setIoLooper(mMyLooper); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); mNativeWrapper = new FakeNativeWrapper(); @@ -124,14 +121,13 @@ public class RequestSadActionTest { mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(mHdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDeviceTv); mHdmiControlService.initService(); mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mNativeWrapper.setPhysicalAddress(0x0000); mTestLooper.dispatchAll(); + mHdmiCecLocalDeviceTv = mHdmiControlService.tv(); mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(); mNativeWrapper.clearResultMessages(); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java index 566a7e0fecce..f5bf30b1ec1e 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java @@ -25,7 +25,6 @@ import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED; import static com.android.server.hdmi.Constants.MESSAGE_ACTIVE_SOURCE; import static com.android.server.hdmi.Constants.MESSAGE_ROUTING_INFORMATION; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTING_INFORMATION; import static com.google.common.truth.Truth.assertThat; @@ -134,7 +133,6 @@ public class RoutingControlActionTest { private FakePowerManagerWrapper mPowerManager; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private static RoutingControlAction createRoutingControlAction(HdmiCecLocalDeviceTv localDevice, TestInputSelectCallback callback) { @@ -150,7 +148,8 @@ public class RoutingControlActionTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), + new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -172,15 +171,12 @@ public class RoutingControlActionTest { } }; - mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); - mHdmiCecLocalDeviceTv.init(); mHdmiControlService.setIoLooper(mMyLooper); mNativeWrapper = new FakeNativeWrapper(); mHdmiCecController = HdmiCecController.createWithNativeWrapper( mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(mHdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDeviceTv); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_AVR, @@ -190,9 +186,9 @@ public class RoutingControlActionTest { mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); mPowerManager = new FakePowerManagerWrapper(context); mHdmiControlService.setPowerManager(mPowerManager); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mNativeWrapper.setPhysicalAddress(0x0000); mTestLooper.dispatchAll(); + mHdmiCecLocalDeviceTv = mHdmiControlService.tv(); mNativeWrapper.clearResultMessages(); mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java index dadf81571e30..e3c8939c81e5 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java @@ -21,7 +21,6 @@ import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED; import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -67,7 +66,6 @@ public class SetAudioVolumeLevelDiscoveryActionTest { private Context mContextSpy; private TestLooper mTestLooper = new TestLooper(); private int mPhysicalAddress = 0x1100; - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mPlaybackLogicalAddress; private TestCallback mTestCallback; @@ -82,7 +80,8 @@ public class SetAudioVolumeLevelDiscoveryActionTest { mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); - mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), new FakeAudioDeviceVolumeManagerWrapper())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); @@ -104,21 +103,16 @@ public class SetAudioVolumeLevelDiscoveryActionTest { mPowerManager = new FakePowerManagerWrapper(mContextSpy); mHdmiControlServiceSpy.setPowerManager(mPowerManager); - mPlaybackDevice = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy); - mPlaybackDevice.init(); - mLocalDevices.add(mPlaybackDevice); - - mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); mTestLooper.dispatchAll(); + mPlaybackDevice = mHdmiControlServiceSpy.playback(); mPlaybackLogicalAddress = mPlaybackDevice.getDeviceInfo().getLogicalAddress(); // Setup specific to these tests mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( Constants.ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV)); mTestLooper.dispatchAll(); - mTestCallback = new TestCallback(); mAction = new SetAudioVolumeLevelDiscoveryAction(mPlaybackDevice, Constants.ADDR_TV, mTestCallback); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java index 1644252e5739..e7557fe3aa43 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java @@ -19,7 +19,6 @@ package com.android.server.hdmi; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; -import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT; import static com.google.common.truth.Truth.assertThat; @@ -28,6 +27,7 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.content.ContextWrapper; +import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.media.AudioManager; import android.os.Looper; @@ -42,7 +42,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.util.ArrayList; import java.util.Collections; /** @@ -61,7 +60,6 @@ public class SystemAudioAutoInitiationActionTest { private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv; private TestLooper mTestLooper = new TestLooper(); - private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mPhysicalAddress; @Before @@ -70,7 +68,8 @@ public class SystemAudioAutoInitiationActionTest { Looper myLooper = mTestLooper.getLooper(); - mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), + mHdmiControlService = new HdmiControlService(mContextSpy, + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), new FakeAudioDeviceVolumeManagerWrapper()) { @Override AudioManager getAudioManager() { @@ -94,15 +93,12 @@ public class SystemAudioAutoInitiationActionTest { } }; - mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); - mHdmiCecLocalDeviceTv.init(); mHdmiControlService.setIoLooper(myLooper); mNativeWrapper = new FakeNativeWrapper(); HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper( mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); mHdmiControlService.setCecController(hdmiCecController); mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); - mLocalDevices.add(mHdmiCecLocalDeviceTv); HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2]; hdmiPortInfos[0] = new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false); @@ -113,10 +109,10 @@ public class SystemAudioAutoInitiationActionTest { mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); mPowerManager = new FakePowerManagerWrapper(mContextSpy); mHdmiControlService.setPowerManager(mPowerManager); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mPhysicalAddress = 0x0000; mNativeWrapper.setPhysicalAddress(mPhysicalAddress); mTestLooper.dispatchAll(); + mHdmiCecLocalDeviceTv = mHdmiControlService.tv(); mPhysicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(); mNativeWrapper.clearResultMessages(); } diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index 65076a372b0b..c68db3460dac 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -19,6 +19,7 @@ package com.android.server.input import android.content.Context import android.content.ContextWrapper import android.hardware.BatteryState.STATUS_CHARGING +import android.hardware.BatteryState.STATUS_DISCHARGING import android.hardware.BatteryState.STATUS_FULL import android.hardware.BatteryState.STATUS_UNKNOWN import android.hardware.input.IInputDeviceBatteryListener @@ -32,6 +33,7 @@ import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.InstrumentationRegistry +import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS import com.android.server.input.BatteryController.UEventManager import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener import org.hamcrest.Description @@ -42,6 +44,8 @@ import org.hamcrest.TypeSafeMatcher import org.hamcrest.core.IsEqual.equalTo import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Rule @@ -63,14 +67,20 @@ import org.mockito.hamcrest.MockitoHamcrest import org.mockito.junit.MockitoJUnit import org.mockito.verification.VerificationMode -private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice = +private fun createInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false, + generation: Int = -1, +): InputDevice = InputDevice.Builder() .setId(deviceId) .setName("Device $deviceId") .setDescriptor("descriptor $deviceId") .setExternal(true) .setHasBattery(hasBattery) - .setGeneration(0) + .setSupportsUsi(supportsUsi) + .setGeneration(generation) .build() // Returns a matcher that helps match member variables of a class. @@ -118,7 +128,10 @@ private fun matchesState( return Matchers.allOf(batteryStateMatchers) } -// Helper used to verify interactions with a mocked battery listener. +private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> = + matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN) + +// Helpers used to verify interactions with a mocked battery listener. private fun IInputDeviceBatteryListener.verifyNotified( deviceId: Int, mode: VerificationMode = times(1), @@ -127,8 +140,21 @@ private fun IInputDeviceBatteryListener.verifyNotified( capacity: Float? = null, eventTime: Long? = null ) { - verify(this, mode).onBatteryStateChanged( - MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime))) + verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode) +} + +private fun IInputDeviceBatteryListener.verifyNotified( + matcher: Matcher<IInputDeviceBatteryState>, + mode: VerificationMode = times(1) +) { + verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher)) +} + +private fun createMockListener(): IInputDeviceBatteryListener { + val listener = mock(IInputDeviceBatteryListener::class.java) + val binder = mock(Binder::class.java) + `when`(listener.asBinder()).thenReturn(binder) + return listener } /** @@ -143,6 +169,8 @@ class BatteryControllerTests { const val PID = 42 const val DEVICE_ID = 13 const val SECOND_DEVICE_ID = 11 + const val USI_DEVICE_ID = 101 + const val SECOND_USI_DEVICE_ID = 102 const val TIMESTAMP = 123456789L } @@ -168,10 +196,11 @@ class BatteryControllerTests { testLooper = TestLooper() val inputManager = InputManager.resetInstance(iInputManager) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) - `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID)) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID)) - `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID)) - .thenReturn(createInputDevice(SECOND_DEVICE_ID)) + `when`(iInputManager.inputDeviceIds).then { + deviceGenerationMap.keys.toIntArray() + } + addInputDevice(DEVICE_ID) + addInputDevice(SECOND_DEVICE_ID) batteryController = BatteryController(context, native, testLooper.looper, uEventManager) batteryController.systemRunning() @@ -180,10 +209,30 @@ class BatteryControllerTests { devicesChangedListener = listenerCaptor.value } - private fun notifyDeviceChanged(deviceId: Int) { - deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1 + private fun notifyDeviceChanged( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false + ) { + val generation = deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") + deviceGenerationMap[deviceId] = generation + + `when`(iInputManager.getInputDevice(deviceId)) + .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation)) val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } - devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + if (::devicesChangedListener.isInitialized) { + devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + } + } + + private fun addInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false + ) { + deviceGenerationMap[deviceId] = 0 + notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } @After @@ -191,13 +240,6 @@ class BatteryControllerTests { InputManager.clearInstance() } - private fun createMockListener(): IInputDeviceBatteryListener { - val listener = mock(IInputDeviceBatteryListener::class.java) - val binder = mock(Binder::class.java) - `when`(listener.asBinder()).thenReturn(binder) - return listener - } - @Test fun testRegisterAndUnregisterBinderLifecycle() { val listener = createMockListener() @@ -303,19 +345,14 @@ class BatteryControllerTests { listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) // If the battery presence for the InputDevice changes, the listener is notified. - `when`(iInputManager.getInputDevice(DEVICE_ID)) - .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false)) - notifyDeviceChanged(DEVICE_ID) + notifyDeviceChanged(DEVICE_ID, hasBattery = false) testLooper.dispatchNext() - listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN, - capacity = Float.NaN) + listener.verifyNotified(isInvalidBatteryState(DEVICE_ID)) // Since the battery is no longer present, the UEventListener should be removed. verify(uEventManager).removeListener(uEventListener.value) // If the battery becomes present again, the listener is notified. - `when`(iInputManager.getInputDevice(DEVICE_ID)) - .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true)) - notifyDeviceChanged(DEVICE_ID) + notifyDeviceChanged(DEVICE_ID, hasBattery = true) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING, capacity = 0.78f) @@ -340,9 +377,17 @@ class BatteryControllerTests { // Move the time forward so that the polling period has elapsed. // The listener should be notified. - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, capacity = 0.80f) + + // Move the time forward so that another polling period has elapsed. + // The battery should still be polled, but there is no change so listeners are not notified. + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f) } @Test @@ -357,7 +402,8 @@ class BatteryControllerTests { // The battery state changed, but we should not be polling for battery changes when the // device is not interactive. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchAll() listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) @@ -368,7 +414,8 @@ class BatteryControllerTests { // Ensure that we continue to poll for battery changes. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90) - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, capacity = 0.90f) } @@ -398,4 +445,93 @@ class BatteryControllerTests { matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)) listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f) } + + @Test + fun testUsiDeviceIsMonitoredPersistently() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + // Even though there is no listener added for this device, it is being monitored. + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // Add and remove a listener for the device. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID) + + // The device is still being monitored. + verify(uEventManager, never()).removeListener(uEventListener.value) + } + + @Test + fun testNoPollingWhenUsiDevicesAreMonitored() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2") + addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + + // Add a listener. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + } + + @Test + fun testExpectedFlowForUsiBattery() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // A USI device's battery state is not valid until the first UEvent notification. + // Add a listener, and ensure it is notified that the battery state is not present. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + + // Ensure that querying for battery state also returns the same invalid result. + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // There is a UEvent signaling a battery change. The battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // There is another UEvent notification. The battery state is now updated. + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64) + uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + + // The battery state is still valid after a millisecond. + testLooper.moveTimeForward(1) + testLooper.dispatchAll() + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + + // The battery is no longer present after the timeout expires. + testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + } } diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index 3bcde6a3aa53..b7f90d43881b 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -113,8 +113,10 @@ import android.app.NotificationManager; import android.app.usage.NetworkStats; import android.app.usage.NetworkStatsManager; import android.app.usage.UsageStatsManagerInternal; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; @@ -134,6 +136,7 @@ import android.net.NetworkTemplate; import android.net.TelephonyNetworkSpecifier; import android.net.wifi.WifiInfo; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.INetworkManagementService; import android.os.PersistableBundle; @@ -152,6 +155,7 @@ import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.MediumTest; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.DataUnit; import android.util.Log; import android.util.Pair; @@ -171,11 +175,12 @@ import com.android.server.LocalServices; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.usage.AppStandbyInternal; -import libcore.io.Streams; - import com.google.common.util.concurrent.AbstractFuture; +import libcore.io.Streams; + import org.junit.After; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -286,6 +291,8 @@ public class NetworkPolicyManagerServiceTest { private NetworkPolicyListenerAnswer mPolicyListener; private NetworkPolicyManagerService mService; + private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>(); + /** * In some of the tests while initializing NetworkPolicyManagerService, * ACTION_RESTRICT_BACKGROUND_CHANGED is broadcasted. This is for capturing that broadcast. @@ -437,6 +444,21 @@ public class NetworkPolicyManagerServiceTest { public void enforceCallingOrSelfPermission(String permission, String message) { // Assume that we're AID_SYSTEM } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, + IntentFilter filter, String broadcastPermission, Handler scheduler) { + mRegisteredReceivers.add(receiver); + return super.registerReceiver(receiver, filter, broadcastPermission, scheduler); + } + + @Override + public Intent registerReceiverForAllUsers(BroadcastReceiver receiver, + IntentFilter filter, String broadcastPermission, Handler scheduler) { + mRegisteredReceivers.add(receiver); + return super.registerReceiverForAllUsers(receiver, filter, broadcastPermission, + scheduler); + } }; setNetpolicyXml(context); @@ -557,6 +579,13 @@ public class NetworkPolicyManagerServiceTest { RecurrenceRule.sClock = Clock.systemDefaultZone(); } + @After + public void unregisterReceivers() throws Exception { + for (BroadcastReceiver receiver : mRegisteredReceivers) { + mServiceContext.unregisterReceiver(receiver); + } + } + @Test public void testTurnRestrictBackgroundOn() throws Exception { assertRestrictBackgroundOff(); @@ -2033,6 +2062,9 @@ public class NetworkPolicyManagerServiceTest { @Test public void testNormalizeTemplate_duplicatedMergedImsiList() { + // This test leads to a Log.wtf, so skip it on eng builds. Otherwise, Log.wtf() would + // result in this process getting killed. + Assume.assumeFalse(Build.IS_ENG); final NetworkTemplate template = new NetworkTemplate.Builder(MATCH_CARRIER) .setSubscriberIds(Set.of(TEST_IMSI)).build(); final String[] mergedImsiGroup = new String[] {TEST_IMSI, TEST_IMSI}; diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index fe4db3a758e3..db2630e2683c 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -87,7 +87,6 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.SystemService; -import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.PowerManagerService.BatteryReceiver; @@ -147,7 +146,6 @@ public class PowerManagerServiceTest { @Mock private SystemPropertiesWrapper mSystemPropertiesMock; @Mock private AppOpsManager mAppOpsManagerMock; @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock; - @Mock private PlatformCompat mPlatformCompat; @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; @@ -321,11 +319,6 @@ public class PowerManagerServiceTest { AppOpsManager createAppOpsManager(Context context) { return mAppOpsManagerMock; } - - @Override - PlatformCompat createPlatformCompat(Context context) { - return mPlatformCompat; - } }); return mService; } @@ -505,9 +498,6 @@ public class PowerManagerServiceTest { String packageName = "pkg.name"; when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(true); when(mContextSpy.checkCallingOrSelfPermission( android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( PackageManager.PERMISSION_GRANTED); @@ -532,23 +522,6 @@ public class PowerManagerServiceTest { null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); - - // Verify that on older platforms only the appOp is necessary and the permission isn't - // checked - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(false); - when(mContextSpy.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( - PackageManager.PERMISSION_DENIED); - forceSleep(); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); - - flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; - mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, - null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); - mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); } @Test @@ -568,7 +541,7 @@ public class PowerManagerServiceTest { int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); } else { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); @@ -577,9 +550,6 @@ public class PowerManagerServiceTest { when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(true); when(mContextSpy.checkCallingOrSelfPermission( android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( PackageManager.PERMISSION_DENIED); @@ -589,7 +559,7 @@ public class PowerManagerServiceTest { flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); } else { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java index 52e9d3a06fe2..34d008201bb2 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java @@ -17,6 +17,7 @@ package com.android.server.timezonedetector.location; import android.service.timezone.TimeZoneProviderEvent; +import android.service.timezone.TimeZoneProviderStatus; /** * Fake implementation of {@link TimeZoneProviderEventPreProcessor} which assumes that all events @@ -31,7 +32,8 @@ public final class FakeTimeZoneProviderEventPreProcessor public TimeZoneProviderEvent preProcess(TimeZoneProviderEvent timeZoneProviderEvent) { if (mIsUncertain) { return TimeZoneProviderEvent.createUncertainEvent( - timeZoneProviderEvent.getCreationElapsedMillis()); + timeZoneProviderEvent.getCreationElapsedMillis(), + TimeZoneProviderStatus.UNKNOWN); } return timeZoneProviderEvent; } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java index 0257ce0fe7b9..ed426cdc9f7e 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java @@ -15,6 +15,12 @@ */ package com.android.server.timezonedetector.location; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_UNKNOWN; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING; + import static com.android.server.timezonedetector.ConfigurationInternal.DETECTION_MODE_MANUAL; import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED; import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED; @@ -48,6 +54,7 @@ import android.annotation.Nullable; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.service.timezone.TimeZoneProviderEvent; +import android.service.timezone.TimeZoneProviderStatus; import android.service.timezone.TimeZoneProviderSuggestion; import android.util.IndentingPrintWriter; @@ -78,8 +85,15 @@ public class LocationTimeZoneProviderControllerTest { createSuggestionEvent(asList("Europe/London")); private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 = createSuggestionEvent(asList("Europe/Paris")); + private static final TimeZoneProviderStatus UNCERTAIN_PROVIDER_STATUS = + new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_UNKNOWN) + .build(); private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT = - TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_TIME_MILLIS); + TimeZoneProviderEvent.createUncertainEvent( + ARBITRARY_TIME_MILLIS, UNCERTAIN_PROVIDER_STATUS); private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT = TimeZoneProviderEvent.createPermanentFailureEvent(ARBITRARY_TIME_MILLIS, "Test"); @@ -1390,12 +1404,17 @@ public class LocationTimeZoneProviderControllerTest { } private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List<String> timeZoneIds) { + TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_NOT_APPLICABLE) + .setConnectivityStatus(DEPENDENCY_STATUS_NOT_APPLICABLE) + .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING) + .build(); + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) + .setTimeZoneIds(timeZoneIds) + .build(); return TimeZoneProviderEvent.createSuggestionEvent( - ARBITRARY_TIME_MILLIS, - new TimeZoneProviderSuggestion.Builder() - .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) - .setTimeZoneIds(timeZoneIds) - .build()); + ARBITRARY_TIME_MILLIS, suggestion, providerStatus); } private static void assertControllerState(LocationTimeZoneProviderController controller, diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java index cb2905d2266a..8429fa4d18d1 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java @@ -33,6 +33,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.platform.test.annotations.Presubmit; import android.service.timezone.TimeZoneProviderEvent; +import android.service.timezone.TimeZoneProviderStatus; import android.service.timezone.TimeZoneProviderSuggestion; import android.util.IndentingPrintWriter; @@ -120,8 +121,9 @@ public class LocationTimeZoneProviderTest { .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) .setTimeZoneIds(Arrays.asList("Europe/London")) .build(); + TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN; TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent( - ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion); + ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, providerStatus); provider.simulateProviderEventReceived(event); currentState = assertAndReturnProviderState( @@ -133,7 +135,8 @@ public class LocationTimeZoneProviderTest { mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN); // Simulate an uncertain event being received. - event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS); + event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS, + TimeZoneProviderStatus.UNKNOWN); provider.simulateProviderEventReceived(event); currentState = assertAndReturnProviderState( @@ -193,12 +196,13 @@ public class LocationTimeZoneProviderTest { .setTimeZoneIds(Arrays.asList("Europe/London")) .build(); TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent( - ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion); + ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, TimeZoneProviderStatus.UNKNOWN); provider.simulateProviderEventReceived(event); provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN); // Simulate an uncertain event being received. - event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS); + event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS, + TimeZoneProviderStatus.UNKNOWN); provider.simulateProviderEventReceived(event); provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN); @@ -235,8 +239,9 @@ public class LocationTimeZoneProviderTest { .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS) .setTimeZoneIds(invalidTimeZoneIds) .build(); + TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN; TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent( - ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion); + ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion, providerStatus); provider.simulateProviderEventReceived(event); provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN); } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java index ab4fe2938bcf..c4786043cc29 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java @@ -16,10 +16,15 @@ package com.android.server.timezonedetector.location; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING; + import static com.google.common.truth.Truth.assertWithMessage; import android.platform.test.annotations.Presubmit; import android.service.timezone.TimeZoneProviderEvent; +import android.service.timezone.TimeZoneProviderStatus; import android.service.timezone.TimeZoneProviderSuggestion; import org.junit.Test; @@ -54,8 +59,14 @@ public class ZoneInfoDbTimeZoneProviderEventPreProcessorTest { for (String timeZone : nonExistingTimeZones) { TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone); + TimeZoneProviderStatus expectedProviderStatus = + new TimeZoneProviderStatus.Builder(event.getTimeZoneProviderStatus()) + .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED) + .build(); + TimeZoneProviderEvent expectedResultEvent = - TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis()); + TimeZoneProviderEvent.createUncertainEvent( + event.getCreationElapsedMillis(), expectedProviderStatus); assertWithMessage(timeZone + " is not a valid time zone") .that(mPreProcessor.preProcess(event)) .isEqualTo(expectedResultEvent); @@ -63,12 +74,17 @@ public class ZoneInfoDbTimeZoneProviderEventPreProcessorTest { } private static TimeZoneProviderEvent timeZoneProviderEvent(String... timeZoneIds) { + TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder() + .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING) + .setConnectivityStatus(DEPENDENCY_STATUS_WORKING) + .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING) + .build(); + TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() + .setTimeZoneIds(Arrays.asList(timeZoneIds)) + .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) + .build(); return TimeZoneProviderEvent.createSuggestionEvent( - ARBITRARY_TIME_MILLIS, - new TimeZoneProviderSuggestion.Builder() - .setTimeZoneIds(Arrays.asList(timeZoneIds)) - .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS) - .build()); + ARBITRARY_TIME_MILLIS, suggestion, providerStatus); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index c3d49e1e5152..bc319db94997 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -242,7 +242,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { private IOnBackInvokedCallback createOnBackInvokedCallback() { return new IOnBackInvokedCallback.Stub() { @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 11ae5d4abaf8..e69418ba908e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2314,6 +2314,8 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(displayWidth, windowConfig.getBounds().width()); assertEquals(displayHeight, windowConfig.getBounds().height()); assertEquals(windowingMode, windowConfig.getWindowingMode()); + assertEquals(Configuration.SCREENLAYOUT_SIZE_NORMAL, + config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK); // test misc display overrides assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest); @@ -2355,6 +2357,8 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(displayWidth, windowConfig.getBounds().width()); assertEquals(displayHeight, windowConfig.getBounds().height()); assertEquals(windowingMode, windowConfig.getWindowingMode()); + assertEquals(Configuration.SCREENLAYOUT_SIZE_LARGE, testDisplayContent + .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK); // test misc display overrides assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest); diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java index fc3962bd0b23..cd4d65d7dab1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java @@ -26,8 +26,10 @@ import android.graphics.ColorSpace; import android.graphics.Matrix; import android.graphics.PointF; import android.hardware.HardwareBuffer; -import android.view.Surface; import android.platform.test.annotations.Presubmit; +import android.view.Surface; + +import com.android.internal.policy.TransitionAnimation; import org.junit.Before; import org.junit.Test; @@ -52,7 +54,8 @@ public class RotationAnimationUtilsTest { public void blackLuma() { Bitmap swBitmap = createBitmap(0); HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap); - float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace); + float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace); + assertEquals(0, borderLuma, 0); } @@ -60,7 +63,7 @@ public class RotationAnimationUtilsTest { public void whiteLuma() { Bitmap swBitmap = createBitmap(1); HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap); - float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace); + float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace); assertEquals(1, borderLuma, 0); } @@ -68,7 +71,7 @@ public class RotationAnimationUtilsTest { public void unevenBitmapDimens() { Bitmap swBitmap = createBitmap(1, BITMAP_WIDTH + 1, BITMAP_HEIGHT + 1); HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap); - float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace); + float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace); assertEquals(1, borderLuma, 0); } @@ -77,7 +80,7 @@ public class RotationAnimationUtilsTest { Bitmap swBitmap = createBitmap(1); setBorderLuma(swBitmap, 0); HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap); - float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace); + float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace); assertEquals(0, borderLuma, 0); } @@ -86,7 +89,7 @@ public class RotationAnimationUtilsTest { Bitmap swBitmap = createBitmap(0); setBorderLuma(swBitmap, 1); HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap); - float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace); + float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace); assertEquals(1, borderLuma, 0); } diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java index 2ae328b0d8e9..394d6e774aa1 100644 --- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java @@ -19,6 +19,7 @@ package com.android.server.usb; import android.annotation.NonNull; import android.content.Context; import android.hardware.usb.UsbConfiguration; +import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; @@ -76,10 +77,10 @@ public final class UsbDirectMidiDevice implements Closeable { // event schedulers for each input port of the physical device private MidiEventScheduler[] mEventSchedulers; - // Arbitrary number for timeout to not continue sending to - // an inactive device. This number tries to balances the number - // of cycles and not being permanently stuck. - private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10; + // Timeout for sending a packet to a device. + // If bulkTransfer times out, retry sending the packet up to 20 times. + private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50; + private static final int BULK_TRANSFER_NUMBER_OF_RETRIES = 20; // Arbitrary number for timeout when closing a thread private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200; @@ -386,10 +387,15 @@ public final class UsbDirectMidiDevice implements Closeable { break; } final UsbRequest response = connectionFinal.requestWait(); - if (response != request) { - Log.w(TAG, "Unexpected response"); + if (response == null) { + Log.w(TAG, "Response is null"); break; } + if (request != response) { + Log.w(TAG, "Skipping response"); + continue; + } + int bytesRead = byteBuffer.position(); if (bytesRead > 0) { @@ -513,9 +519,47 @@ public final class UsbDirectMidiDevice implements Closeable { convertedArray.length); } - connectionFinal.bulkTransfer(endpointFinal, convertedArray, - convertedArray.length, - BULK_TRANSFER_TIMEOUT_MILLISECONDS); + boolean isInterrupted = false; + // Split the packet into multiple if they are greater than the + // endpoint's max packet size. + for (int curPacketStart = 0; + curPacketStart < convertedArray.length && + isInterrupted == false; + curPacketStart += endpointFinal.getMaxPacketSize()) { + int transferResult = -1; + int retryCount = 0; + int curPacketSize = Math.min(endpointFinal.getMaxPacketSize(), + convertedArray.length - curPacketStart); + + // Keep trying to send the packet until the result is + // successful or until the retry limit is reached. + while (transferResult < 0 && retryCount <= + BULK_TRANSFER_NUMBER_OF_RETRIES) { + transferResult = connectionFinal.bulkTransfer( + endpointFinal, + convertedArray, + curPacketStart, + curPacketSize, + BULK_TRANSFER_TIMEOUT_MILLISECONDS); + retryCount++; + + if (Thread.currentThread().interrupted()) { + Log.w(TAG, "output thread interrupted after send"); + isInterrupted = true; + break; + } + if (transferResult < 0) { + Log.d(TAG, "retrying packet. retryCount = " + + retryCount + " result = " + transferResult); + if (retryCount > BULK_TRANSFER_NUMBER_OF_RETRIES) { + Log.w(TAG, "Skipping packet because timeout"); + } + } + } + } + if (isInterrupted == true) { + break; + } eventSchedulerFinal.addEventToPool(event); } } catch (NullPointerException e) { diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java index 5179babbd31d..76d2b7d8fce5 100644 --- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java +++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java @@ -206,6 +206,8 @@ public final class TelephonyUtils { return "DATA_ON_NON_DEFAULT_DURING_VOICE_CALL"; case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED: return "MMS_ALWAYS_ALLOWED"; + case TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH: + return "AUTO_DATA_SWITCH"; default: return "UNKNOWN(" + mobileDataPolicy + ")"; } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index fcb76b3920fe..d314a6579b58 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8715,6 +8715,8 @@ public class CarrierConfigManager { * premium capabilities should be blocked when * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} * returns a failure due to user action or timeout. + * The maximum number of network boost notifications to show the user are defined in + * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}. * * The default value is 30 minutes. * @@ -8726,6 +8728,22 @@ public class CarrierConfigManager { "premium_capability_notification_backoff_hysteresis_time_millis_long"; /** + * The maximum number of times that we display the notification for a network boost via premium + * capabilities when {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} + * returns a failure due to user action or timeout. + * + * An int array with 2 values: {max_notifications_per_day, max_notifications_per_month}. + * + * The default value is {2, 10}, meaning we display a maximum of 2 network boost notifications + * per day and 10 notifications per month. + * + * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED + * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT + */ + public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = + "premium_capability_maximum_notification_count_int_array"; + + /** * The amount of time in milliseconds that the purchase request should be throttled when * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} * returns a failure due to the carrier. @@ -8752,6 +8770,20 @@ public class CarrierConfigManager { "premium_capability_purchase_url_string"; /** + * Whether to allow premium capabilities to be purchased when the device is connected to LTE. + * If this is {@code true}, applications can call + * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} + * when connected to {@link TelephonyManager#NETWORK_TYPE_LTE} to purchase and use + * premium capabilities. + * If this is {@code false}, applications can only purchase and use premium capabilities when + * conencted to {@link TelephonyManager#NETWORK_TYPE_NR}. + * + * This is {@code false} by default. + */ + public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = + "premium_capability_supported_on_lte_bool"; + + /** * IWLAN handover rules that determine whether handover is allowed or disallowed between * cellular and IWLAN. * @@ -9432,15 +9464,18 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false); sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true); sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false); - sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[]{}); + sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {}); sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); + sDefaults.putIntArray(KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY, + new int[] {2, 10}); sDefaults.putLong( KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null); + sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false); sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{ "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, " + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"}); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index eb3affcd3322..439eaa69e771 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -54,6 +54,7 @@ import android.os.Looper; import android.os.ParcelUuid; import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Telephony.SimInfo; import android.telephony.euicc.EuiccManager; import android.telephony.ims.ImsMmTelManager; @@ -4154,4 +4155,79 @@ public class SubscriptionManager { return "UNKNOWN(" + usageSetting + ")"; } } + + /** + * Set userHandle for a subscription. + * + * Used to set an association between a subscription and a user on the device so that voice + * calling and SMS from that subscription can be associated with that user. + * Data services are always shared between users on the device. + * + * @param subscriptionId the subId of the subscription. + * @param userHandle the userHandle associated with the subscription. + * Pass {@code null} user handle to clear the association. + * + * @throws IllegalArgumentException if subscription is invalid. + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) + public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) { + if (!isValidSubscriptionId(subscriptionId)) { + throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: " + + subscriptionId); + } + + try { + ISub iSub = TelephonyManager.getSubscriptionService(); + if (iSub != null) { + iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName()); + } else { + throw new IllegalStateException("[setUserHandle]: " + + "subscription service unavailable"); + } + } catch (RemoteException ex) { + ex.rethrowAsRuntimeException(); + } + } + + /** + * Get UserHandle of this subscription. + * + * Used to get user handle associated with this subscription. + * + * @param subscriptionId the subId of the subscription. + * @return userHandle associated with this subscription + * or {@code null} if subscription is not associated with any user. + * + * @throws IllegalArgumentException if subscription is invalid. + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) + public @Nullable UserHandle getUserHandle(int subscriptionId) { + if (!isValidSubscriptionId(subscriptionId)) { + throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: " + + subscriptionId); + } + + try { + ISub iSub = TelephonyManager.getSubscriptionService(); + if (iSub != null) { + return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName()); + } else { + throw new IllegalStateException("[getUserHandle]: " + + "subscription service unavailable"); + } + } catch (RemoteException ex) { + ex.rethrowAsRuntimeException(); + } + return null; + } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index f3d48a849b0f..9ecebf1a28f3 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -54,6 +54,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; @@ -15626,11 +15627,29 @@ public class TelephonyManager { public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2; /** + * Allow switching mobile data to the non-default SIM if the non-default SIM has better + * availability. + * + * This is used for temporarily allowing data on the non-default data SIM when on-default SIM + * has better availability on DSDS devices, where better availability means strong + * signal/connectivity. + * If this policy is enabled, data will be temporarily enabled on the non-default data SIM, + * including during any voice calls(equivalent to enabling + * {@link #MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL}). + * + * This policy can be enabled and disabled via {@link #setMobileDataPolicyEnabled}. + * @hide + */ + @SystemApi + public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3; + + /** * @hide */ @IntDef(prefix = { "MOBILE_DATA_POLICY_" }, value = { MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL, MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED, + MOBILE_DATA_POLICY_AUTO_DATA_SWITCH, }) @Retention(RetentionPolicy.SOURCE) public @interface MobileDataPolicy { } @@ -17115,11 +17134,12 @@ public class TelephonyManager { } /** - * A premium capability boosting the network to allow real-time interactive traffic. - * Corresponds to NetworkCapabilities#NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC. + * A premium capability that boosts the network to allow for real-time interactive traffic + * by prioritizing low latency communication. + * Corresponds to {@link NetworkCapabilities#NET_CAPABILITY_PRIORITIZE_LATENCY}. */ - // TODO(b/245748544): add @link once NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC is defined. - public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; + public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = + NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY; /** * Purchasable premium capabilities. @@ -17127,7 +17147,7 @@ public class TelephonyManager { */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "PREMIUM_CAPABILITY_" }, value = { - PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC}) + PREMIUM_CAPABILITY_PRIORITIZE_LATENCY}) public @interface PremiumCapability {} /** @@ -17139,8 +17159,8 @@ public class TelephonyManager { */ public static String convertPremiumCapabilityToString(@PremiumCapability int capability) { switch (capability) { - case PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC: - return "REALTIME_INTERACTIVE_TRAFFIC"; + case PREMIUM_CAPABILITY_PRIORITIZE_LATENCY: + return "PRIORITIZE_LATENCY"; default: return "UNKNOWN (" + capability + ")"; } @@ -17178,11 +17198,18 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; /** - * Purchase premium capability failed because the request is throttled for the amount of time + * Purchase premium capability failed because the request is throttled. + * If purchasing premium capabilities is throttled, it will be for the amount of time * specified by {@link CarrierConfigManager - * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG} - * or {@link CarrierConfigManager * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}. + * If displaying the network boost notification is throttled, it will be for the amount of time + * specified by {@link CarrierConfigManager + * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_INT_ARRAY}. + * If a foreground application requests premium capabilities, the network boost notification + * will be displayed to the user regardless of the throttled status. + * We will show the network boost notification to the user up to the daily and monthly maximum + * number of times specified by {@link CarrierConfigManager + * #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}. * Subsequent attempts will return the same error until the request is no longer throttled * or throttling conditions change. */ @@ -17202,10 +17229,14 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; /** - * Purchase premium capability failed because the user disabled the feature. - * Subsequent attempts will return the same error until the user re-enables the feature. + * Purchase premium capability failed because a foreground application requested the same + * capability. The notification for the current application will be dismissed and a new + * notification will be displayed to the user for the foreground application. + * Subsequent attempts will return + * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS} until the foreground + * application's request is completed. */ - public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; /** * Purchase premium capability failed because the user canceled the operation. @@ -17252,7 +17283,8 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; /** - * Purchase premium capability failed because the telephony service is down or unavailable. + * Purchase premium capability failed because the telephony service is unavailable + * or there was an error in the phone process. * Subsequent attempts will return the same error until request conditions are satisfied. */ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; @@ -17274,6 +17306,14 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; /** + * Purchase premium capability failed because the request was not made on the default data + * subscription, indicated by {@link SubscriptionManager#getDefaultDataSubscriptionId()}. + * Subsequent attempts will return the same error until the request is made on the default + * data subscription. + */ + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; + + /** * Results of the purchase premium capability request. * @hide */ @@ -17283,14 +17323,15 @@ public class TelephonyManager { PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED, PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS, - PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED, + PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN, PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED, PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED, PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR, PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT, PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED, PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE, - PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED}) + PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED, + PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA}) public @interface PurchasePremiumCapabilityResult {} /** @@ -17311,8 +17352,8 @@ public class TelephonyManager { return "ALREADY_PURCHASED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS: return "ALREADY_IN_PROGRESS"; - case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED: - return "USER_DISABLED"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN: + return "OVERRIDDEN"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED: return "USER_CANCELED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED: @@ -17329,6 +17370,8 @@ public class TelephonyManager { return "NETWORK_NOT_AVAILABLE"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED: return "NETWORK_CONGESTED"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA: + return "NOT_DEFAULT_DATA"; default: return "UNKNOWN (" + result + ")"; } @@ -17346,7 +17389,7 @@ public class TelephonyManager { * @param callback The result of the purchase request. * One of {@link PurchasePremiumCapabilityResult}. * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE. - * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid + * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid. */ @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE) public void purchasePremiumCapability(@PremiumCapability int capability, diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 917f35bc1b82..0211a7f5c5c5 100755 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -18,6 +18,7 @@ package com.android.internal.telephony; import android.telephony.SubscriptionInfo; import android.os.ParcelUuid; +import android.os.UserHandle; import com.android.internal.telephony.ISetOpportunisticDataCallback; interface ISub { @@ -316,4 +317,28 @@ interface ISub { * @throws SecurityException if doesn't have MODIFY_PHONE_STATE or Carrier Privileges */ int setUsageSetting(int usageSetting, int subId, String callingPackage); + + /** + * Set userHandle for this subscription. + * + * @param userHandle the user handle for this subscription + * @param subId the unique SubscriptionInfo index in database + * @param callingPackage The package making the IPC. + * + * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION + * @throws IllegalArgumentException if subId is invalid. + */ + int setUserHandle(in UserHandle userHandle, int subId, String callingPackage); + + /** + * Get UserHandle for this subscription + * + * @param subId the unique SubscriptionInfo index in database + * @param callingPackage the package making the IPC + * @return userHandle associated with this subscription. + * + * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION + * @throws IllegalArgumentException if subId is invalid. + */ + UserHandle getUserHandle(int subId, String callingPackage); } diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java index 916551a8ce6d..79a2f1f5f4de 100644 --- a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java +++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java @@ -75,6 +75,7 @@ public class MainActivity extends Activity { String rollbackStatus = "FAILED"; if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) { rollbackStatus = "SUCCESS"; + mTriggerRollbackButton.setClickable(false); } makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus); }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED); |