diff options
660 files changed, 21246 insertions, 6606 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 17b975bf1311..c38c88749bdb 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1140,7 +1140,6 @@ package android.hardware.camera2 { public final class CameraManager { method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; - field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L } public abstract static class CameraManager.AvailabilityCallback { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d24b677b1d72..03a97ba1d977 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -997,6 +997,8 @@ public class Activity extends ContextThemeWrapper private ComponentCallbacksController mCallbacksController; + @Nullable private IVoiceInteractionManagerService mVoiceInteractionManagerService; + private final WindowControllerCallback mWindowControllerCallback = new WindowControllerCallback() { /** @@ -1606,18 +1608,17 @@ public class Activity extends ContextThemeWrapper private void notifyVoiceInteractionManagerServiceActivityEvent( @VoiceInteractionSession.VoiceInteractionActivityEventType int type) { - - final IVoiceInteractionManagerService service = - IVoiceInteractionManagerService.Stub.asInterface( - ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); - if (service == null) { - Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get " - + "VoiceInteractionManagerService"); - return; + if (mVoiceInteractionManagerService == null) { + mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface( + ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); + if (mVoiceInteractionManagerService == null) { + Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get " + + "VoiceInteractionManagerService"); + return; + } } - try { - service.notifyActivityEventChanged(mToken, type); + mVoiceInteractionManagerService.notifyActivityEventChanged(mToken, type); } catch (RemoteException e) { // Empty } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7f4af8b3dc78..834d186eebb7 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -7948,8 +7948,6 @@ public class Notification implements Parcelable * @hide */ public MessagingStyle setShortcutIcon(@Nullable Icon conversationIcon) { - // TODO(b/228941516): This icon should be downscaled to avoid using too much memory, - // see reduceImageSizes. mShortcutIcon = conversationIcon; return this; } diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index a51b9d3956df..27f9f54d9522 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -1406,6 +1406,17 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * Return the number of entries in the cache. This is used for testing and has package-only + * visibility. + * @hide + */ + public int size() { + synchronized (mLock) { + return mCache.size(); + } + } + + /** * Returns a list of caches alive at the current time. */ @GuardedBy("sGlobalLock") @@ -1612,8 +1623,12 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void onTrimMemory() { - for (PropertyInvalidatedCache pic : getActiveCaches()) { - pic.clear(); + ArrayList<PropertyInvalidatedCache> activeCaches; + synchronized (sGlobalLock) { + activeCaches = getActiveCaches(); + } + for (int i = 0; i < activeCaches.size(); i++) { + activeCaches.get(i).clear(); } } } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 67d51481830f..f36012104541 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6789,6 +6789,8 @@ public class DevicePolicyManager { * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}. + * + * @throws SecurityException if called on a parent instance. */ public int getStorageEncryptionStatus() { throwIfParentInstance("getStorageEncryptionStatus"); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 2ea0d8235548..a320f1e9509c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -11488,7 +11488,7 @@ public class Intent implements Parcelable, Cloneable { private void toUriInner(StringBuilder uri, String scheme, String defAction, String defPackage, int flags) { if (scheme != null) { - uri.append("scheme=").append(scheme).append(';'); + uri.append("scheme=").append(Uri.encode(scheme)).append(';'); } if (mAction != null && !mAction.equals(defAction)) { uri.append("action=").append(Uri.encode(mAction)).append(';'); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 9e5e8deda84b..2e3b5d286138 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1058,6 +1058,41 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id /** + * This change id excludes the packages it is applied to from the camera compat force rotation + * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION = + 263959004L; // buganizer id + + /** + * This change id excludes the packages it is applied to from activity refresh after camera + * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for + * context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id + + /** + * This change id makes the packages it is applied to do activity refresh after camera compat + * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed -> + * ... -> stopped -> ... -> resumed" cycle. See + * com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + 264301586L; // buganizer id + + /** * This change id is the gatekeeper for all treatments that force a given min aspect ratio. * Enabling this change will allow the following min aspect ratio treatments to be applied: * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM diff --git a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java index 41f2f9c28349..b86b97c76740 100644 --- a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java +++ b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java @@ -17,7 +17,7 @@ package android.database.sqlite; /** - * Thrown if the the bind or column parameter index is out of range + * Thrown if the bind or column parameter index is out of range. */ public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException { public SQLiteBindOrColumnIndexOutOfRangeException() {} diff --git a/core/java/android/database/sqlite/SQLiteDiskIOException.java b/core/java/android/database/sqlite/SQLiteDiskIOException.java index 01b2069c23db..152d90a76ba6 100644 --- a/core/java/android/database/sqlite/SQLiteDiskIOException.java +++ b/core/java/android/database/sqlite/SQLiteDiskIOException.java @@ -17,7 +17,7 @@ package android.database.sqlite; /** - * An exception that indicates that an IO error occured while accessing the + * Indicates that an IO error occurred while accessing the * SQLite database file. */ public class SQLiteDiskIOException extends SQLiteException { diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 5291d2b73891..7ccf07a2a764 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -29,7 +29,6 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.ActivityThread; import android.app.AppOpsManager; -import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.ImageFormat; @@ -47,7 +46,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; @@ -284,14 +282,6 @@ public class Camera { */ public native static int getNumberOfCameras(); - private static final boolean sLandscapeToPortrait = - SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false); - - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && sLandscapeToPortrait; - } - /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. @@ -301,7 +291,8 @@ public class Camera { * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); _getCameraInfo(cameraId, overrideToPortrait, cameraInfo); IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); @@ -498,7 +489,8 @@ public class Camera { mEventHandler = null; } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); return native_setup(new WeakReference<Camera>(this), cameraId, ActivityThread.currentOpPackageName(), overrideToPortrait); } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index ed1e9e5f6228..30c410144949 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -115,8 +115,14 @@ public final class CameraManager { @ChangeId @Overridable @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) - @TestApi - public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; + public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; + + /** + * Package-level opt in/out for the above. + * @hide + */ + public static final String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT = + "android.camera.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT"; /** * System property for allowing the above @@ -392,6 +398,23 @@ public final class CameraManager { * except that it uses {@link java.util.concurrent.Executor} as an argument * instead of {@link android.os.Handler}.</p> * + * <p>Note: If the order between some availability callbacks matters, the implementation of the + * executor should handle those callbacks in the same thread to maintain the callbacks' order. + * Some examples are:</p> + * + * <ul> + * + * <li>{@link AvailabilityCallback#onCameraAvailable} and + * {@link AvailabilityCallback#onCameraUnavailable} of the same camera ID.</li> + * + * <li>{@link AvailabilityCallback#onCameraAvailable} or + * {@link AvailabilityCallback#onCameraUnavailable} of a logical multi-camera, and {@link + * AvailabilityCallback#onPhysicalCameraUnavailable} or + * {@link AvailabilityCallback#onPhysicalCameraAvailable} of its physical + * cameras.</li> + * + * </ul> + * * @param executor The executor which will be used to invoke the callback. * @param callback the new callback to send camera availability notices to * @@ -608,7 +631,7 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { @@ -728,7 +751,7 @@ public final class CameraManager { "Camera service is currently unavailable"); } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); cameraUser = cameraService.connectDevice(callbacks, cameraId, mContext.getOpPackageName(), mContext.getAttributionTag(), uid, oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, @@ -1160,9 +1183,26 @@ public final class CameraManager { return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId); } - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && CameraManagerGlobal.sLandscapeToPortrait; + /** + * @hide + */ + public static boolean shouldOverrideToPortrait(@Nullable Context context) { + if (!CameraManagerGlobal.sLandscapeToPortrait) { + return false; + } + + if (context != null) { + PackageManager packageManager = context.getPackageManager(); + + try { + return packageManager.getProperty(context.getOpPackageName(), + PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + // No such property + } + } + + return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT); } /** diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index a6c79b3a289f..0c2468e65577 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -87,6 +87,7 @@ public class CameraDeviceImpl extends CameraDevice // TODO: guard every function with if (!mRemoteDevice) check (if it was closed) private ICameraDeviceUserWrapper mRemoteDevice; + private boolean mRemoteDeviceInit = false; // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock = new Object(); // access from this class and Session only! @@ -338,6 +339,8 @@ public class CameraDeviceImpl extends CameraDevice mDeviceExecutor.execute(mCallOnOpened); mDeviceExecutor.execute(mCallOnUnconfigured); + + mRemoteDeviceInit = true; } } @@ -1754,8 +1757,8 @@ public class CameraDeviceImpl extends CameraDevice } synchronized(mInterfaceLock) { - if (mRemoteDevice == null) { - return; // Camera already closed + if (mRemoteDevice == null && mRemoteDeviceInit) { + return; // Camera already closed, user is not interested in errors anymore. } // Redirect device callback to the offline session in case we are in the middle diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index dba1a5e8dfc6..6a667fe39974 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -251,6 +251,10 @@ public final class DeviceStateManager { @Nullable private Boolean lastResult; + public FoldStateListener(Context context) { + this(context, folded -> {}); + } + public FoldStateListener(Context context, Consumer<Boolean> listener) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); @@ -266,5 +270,10 @@ public final class DeviceStateManager { mDelegate.accept(folded); } } + + @Nullable + public Boolean getFolded() { + return lastResult; + } } } diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index e1d15defad38..125bdaf07b90 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -74,6 +74,7 @@ interface IUserManager { String getUserAccount(int userId); void setUserAccount(int userId, String accountName); long getUserCreationTime(int userId); + int getUserSwitchability(int userId); boolean isUserSwitcherEnabled(int mUserId); boolean isRestricted(int userId); boolean canHaveRestrictedProfile(int userId); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index e5b8472be939..df82a546b219 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -58,7 +58,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.location.LocationManager; import android.provider.Settings; -import android.telephony.TelephonyManager; import android.util.AndroidException; import android.util.ArraySet; import android.util.Log; @@ -595,8 +594,11 @@ public class UserManager { /** * Specifies if a user is disallowed from transferring files over USB. * - * <p>This restriction can only be set by a device owner, a profile owner on the primary - * user or a profile owner of an organization-owned managed profile on the parent profile. + * <p>This restriction can only be set by a <a href="https://developers.google.com/android/work/terminology#device_owner_do"> + * device owner</a> or a <a href="https://developers.google.com/android/work/terminology#profile_owner_po"> + * profile owner</a> on the primary user's profile or a profile owner of an organization-owned + * <a href="https://developers.google.com/android/work/terminology#managed_profile"> + * managed profile</a> on the parent profile. * When it is set by a device owner, it applies globally. When it is set by a profile owner * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from transferring files over USB. No other @@ -1687,7 +1689,7 @@ public class UserManager { public static final int SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED = 1 << 2; /** - * Result returned in {@link #getUserSwitchability()} indicating user swichability. + * Result returned in {@link #getUserSwitchability()} indicating user switchability. * @hide */ @Retention(RetentionPolicy.SOURCE) @@ -2054,25 +2056,16 @@ public class UserManager { * @hide */ @Deprecated - @RequiresPermission(allOf = { - Manifest.permission.READ_PHONE_STATE, - Manifest.permission.MANAGE_USERS}, // Can be INTERACT_ACROSS_USERS instead. - conditional = true) + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.INTERACT_ACROSS_USERS}) @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @UserHandleAware public boolean canSwitchUsers() { - boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt( - mContext.getContentResolver(), - Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0; - boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM); - boolean inCall = false; - TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); - if (telephonyManager != null) { - inCall = telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE; + try { + return mService.getUserSwitchability(mUserId) == SWITCHABILITY_STATUS_OK; + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); } - boolean isUserSwitchDisallowed = hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId); - return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall - && !isUserSwitchDisallowed; } /** @@ -2106,34 +2099,14 @@ public class UserManager { * @return A {@link UserSwitchabilityResult} flag indicating if the user is switchable. * @hide */ - @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE, - android.Manifest.permission.MANAGE_USERS, - android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.INTERACT_ACROSS_USERS}) public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) { - final TelephonyManager tm = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - - int flags = SWITCHABILITY_STATUS_OK; - if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) { - flags |= SWITCHABILITY_STATUS_USER_IN_CALL; - } - if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, userHandle)) { - flags |= SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED; - } - - // System User is always unlocked in Headless System User Mode, so ignore this flag - if (!isHeadlessSystemUserMode()) { - final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt( - mContext.getContentResolver(), - Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0; - final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM); - - if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) { - flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED; - } + try { + return mService.getUserSwitchability(userHandle.getIdentifier()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); } - - return flags; } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 85e3aee02b40..a5092fb8f17c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9233,6 +9233,14 @@ public final class Settings { public static final int DOCK_SETUP_PROMPTED = 3; /** + * Indicates that the user has started dock setup but never finished it. + * One of the possible states for {@link #DOCK_SETUP_STATE}. + * + * @hide + */ + public static final int DOCK_SETUP_INCOMPLETE = 4; + + /** * Indicates that the user has completed dock setup. * One of the possible states for {@link #DOCK_SETUP_STATE}. * @@ -9240,6 +9248,14 @@ public final class Settings { */ public static final int DOCK_SETUP_COMPLETED = 10; + /** + * Indicates that dock setup timed out before the user could complete it. + * One of the possible states for {@link #DOCK_SETUP_STATE}. + * + * @hide + */ + public static final int DOCK_SETUP_TIMED_OUT = 11; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -9247,7 +9263,9 @@ public final class Settings { DOCK_SETUP_STARTED, DOCK_SETUP_PAUSED, DOCK_SETUP_PROMPTED, - DOCK_SETUP_COMPLETED + DOCK_SETUP_INCOMPLETE, + DOCK_SETUP_COMPLETED, + DOCK_SETUP_TIMED_OUT }) public @interface DockSetupState { } @@ -9819,11 +9837,12 @@ public final class Settings { "fingerprint_side_fps_auth_downtime"; /** - * Whether or not a SFPS device is required to be interactive for auth to unlock the device. + * Whether or not a SFPS device is enabling the performant auth setting. + * The "_V2" suffix was added to re-introduce the default behavior for + * users. See b/265264294 fore more details. * @hide */ - public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED = - "sfps_require_screen_on_to_auth_enabled"; + public static final String SFPS_PERFORMANT_AUTH_ENABLED = "sfps_performant_auth_enabled_v2"; /** * Whether or not debugging is enabled. diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java index 4f37cd91b11f..a2ffa5d34219 100644 --- a/core/java/android/service/appprediction/AppPredictionService.java +++ b/core/java/android/service/appprediction/AppPredictionService.java @@ -328,7 +328,7 @@ public abstract class AppPredictionService extends Service { Slog.e(TAG, "Callback is null, likely the binder has died."); return false; } - return mCallback.equals(callback); + return mCallback.asBinder().equals(callback.asBinder()); } public void destroy() { diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java index 0fb9f57f5f57..b0e847cd53f9 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -166,7 +166,7 @@ public final class FillEventHistory implements Parcelable { } /** - * Description of an event that occured after the latest call to + * Description of an event that occurred after the latest call to * {@link FillCallback#onSuccess(FillResponse)}. */ public static final class Event { diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 4b25c8832068..182a49758892 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -52,7 +52,7 @@ public final class Adjustment implements Parcelable { /** @hide */ @StringDef (prefix = { "KEY_" }, value = { KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA, - KEY_TEXT_REPLIES, KEY_USER_SENTIMENT + KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL }) @Retention(RetentionPolicy.SOURCE) public @interface Keys {} @@ -122,6 +122,19 @@ public final class Adjustment implements Parcelable { public static final String KEY_IMPORTANCE = "key_importance"; /** + * Weaker than {@link #KEY_IMPORTANCE}, this adjustment suggests an importance rather than + * mandates an importance change. + * + * A notification listener can interpet this suggestion to show the user a prompt to change + * notification importance for the notification (or type, or app) moving forward. + * + * Data type: int, one of importance values e.g. + * {@link android.app.NotificationManager#IMPORTANCE_MIN}. + * @hide + */ + public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal"; + + /** * Data type: float, a ranking score from 0 (lowest) to 1 (highest). * Used to rank notifications inside that fall under the same classification (i.e. alerting, * silenced). diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index ad2e9d510998..dc4cb9f09835 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1711,6 +1711,8 @@ public abstract class NotificationListenerService extends Service { private ShortcutInfo mShortcutInfo; private @RankingAdjustment int mRankingAdjustment; private boolean mIsBubble; + // Notification assistant importance suggestion + private int mProposedImportance; private static final int PARCEL_VERSION = 2; @@ -1748,6 +1750,7 @@ public abstract class NotificationListenerService extends Service { out.writeParcelable(mShortcutInfo, flags); out.writeInt(mRankingAdjustment); out.writeBoolean(mIsBubble); + out.writeInt(mProposedImportance); } /** @hide */ @@ -1786,6 +1789,7 @@ public abstract class NotificationListenerService extends Service { mShortcutInfo = in.readParcelable(cl, android.content.pm.ShortcutInfo.class); mRankingAdjustment = in.readInt(); mIsBubble = in.readBoolean(); + mProposedImportance = in.readInt(); } @@ -1878,6 +1882,22 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns the proposed importance provided by the {@link NotificationAssistantService}. + * + * This can be used to suggest that the user change the importance of this type of + * notification moving forward. A value of + * {@link NotificationManager#IMPORTANCE_UNSPECIFIED} means that the NAS has not recommended + * a change to the importance, and no UI should be shown to the user. See + * {@link Adjustment#KEY_IMPORTANCE_PROPOSAL}. + * + * @return the importance of the notification + * @hide + */ + public @NotificationManager.Importance int getProposedImportance() { + return mProposedImportance; + } + + /** * If the system has overridden the group key, then this will be non-null, and this * key should be used to bundle notifications. */ @@ -2041,7 +2061,7 @@ public abstract class NotificationListenerService extends Service { boolean noisy, ArrayList<Notification.Action> smartActions, ArrayList<CharSequence> smartReplies, boolean canBubble, boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo, - int rankingAdjustment, boolean isBubble) { + int rankingAdjustment, boolean isBubble, int proposedImportance) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -2067,6 +2087,7 @@ public abstract class NotificationListenerService extends Service { mShortcutInfo = shortcutInfo; mRankingAdjustment = rankingAdjustment; mIsBubble = isBubble; + mProposedImportance = proposedImportance; } /** @@ -2107,7 +2128,8 @@ public abstract class NotificationListenerService extends Service { other.mIsConversation, other.mShortcutInfo, other.mRankingAdjustment, - other.mIsBubble); + other.mIsBubble, + other.mProposedImportance); } /** @@ -2166,7 +2188,8 @@ public abstract class NotificationListenerService extends Service { && Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()), (other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId())) && Objects.equals(mRankingAdjustment, other.mRankingAdjustment) - && Objects.equals(mIsBubble, other.mIsBubble); + && Objects.equals(mIsBubble, other.mIsBubble) + && Objects.equals(mProposedImportance, other.mProposedImportance); } } diff --git a/core/java/android/service/smartspace/SmartspaceService.java b/core/java/android/service/smartspace/SmartspaceService.java index 3a148dffe6d6..b13a069116af 100644 --- a/core/java/android/service/smartspace/SmartspaceService.java +++ b/core/java/android/service/smartspace/SmartspaceService.java @@ -302,7 +302,7 @@ public abstract class SmartspaceService extends Service { Slog.e(TAG, "Callback is null, likely the binder has died."); return false; } - return mCallback.equals(callback); + return mCallback.asBinder().equals(callback.asBinder()); } @Override diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java index a1d6cc8e283a..6da0b63dbc1f 100644 --- a/core/java/android/text/TextShaper.java +++ b/core/java/android/text/TextShaper.java @@ -173,7 +173,7 @@ public class TextShaper { private TextShaper() {} /** - * An consumer interface for accepting text shape result. + * A consumer interface for accepting text shape result. */ public interface GlyphsConsumer { /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 5f04d5846977..f0a14ae296ba 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -73,6 +73,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FIT_INSETS_CO import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; @@ -1273,7 +1274,7 @@ public final class ViewRootImpl implements ViewParent, mTmpFrames.attachedFrame = attachedFrame; mTmpFrames.compatScale = compatScale[0]; mInvCompatScale = 1f / compatScale[0]; - } catch (RemoteException e) { + } catch (RemoteException | RuntimeException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; @@ -2768,7 +2769,7 @@ public final class ViewRootImpl implements ViewParent, * TODO(b/260382739): Apply this to all windows. */ private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) { - return lp.type == TYPE_NOTIFICATION_SHADE; + return (lp.privateFlags & PRIVATE_FLAG_OPTIMIZE_MEASURE) != 0; } private Rect getWindowBoundsInsetSystemBars() { @@ -8749,6 +8750,10 @@ public final class ViewRootImpl implements ViewParent, mAdded = false; AnimationHandler.removeRequestor(this); } + if (mSyncBufferCallback != null) { + mSyncBufferCallback.onBufferReady(null); + mSyncBufferCallback = null; + } WindowManagerGlobal.getInstance().doRemoveView(this); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index ed9cb00db290..a37c24499aff 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -853,6 +853,143 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded from the + * camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may apply the force rotation + * treatment to fixed orientation activities. Device manufacturers can exclude packages from the + * treatment using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not apply the force rotation + * treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded + * from the activity "refresh" after the camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context). + * This allows to clear cached values in apps (e.g. display or camera rotation) that influence + * camera preview and can lead to sideways or stretching issues persisting even after force + * rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may "refresh" activity after + * the force rotation treatment. Device manufacturers can exclude packages from the "refresh" + * using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be or shouldn't be + * "refreshed" after the camera compatibility force rotation treatment using "paused -> + * resumed" cycle rather than "stopped -> resumed". + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden by device manufacturers or using this property). This allows to clear cached + * values in apps (e.g., display or camera rotation) that influence camera preview and can lead + * to sideways or stretching issues persisting even after force rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed" + * cycle using their discretion to improve display compatibility. + * + * <p>With this property set to {@code true}, the system will "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device + * manufacturer adds the corresponding override. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; @@ -2443,6 +2580,15 @@ public interface WindowManager extends ViewManager { public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100; /** + * Flag to indicate that the view hierarchy of the window can only be measured when + * necessary. If a window size can be known by the LayoutParams, we can use the size to + * relayout window, and we don't have to measure the view hierarchy before laying out the + * views. This reduces the chances to perform measure. + * {@hide} + */ + public static final int PRIVATE_FLAG_OPTIMIZE_MEASURE = 0x00000200; + + /** * Flag that prevents the wallpaper behind the current window from receiving touch events. * * {@hide} @@ -2644,6 +2790,7 @@ public interface WindowManager extends ViewManager { PRIVATE_FLAG_NO_MOVE_ANIMATION, PRIVATE_FLAG_COMPATIBLE_WINDOW, PRIVATE_FLAG_SYSTEM_ERROR, + PRIVATE_FLAG_OPTIMIZE_MEASURE, PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR, PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT, @@ -2704,6 +2851,10 @@ public interface WindowManager extends ViewManager { equals = PRIVATE_FLAG_SYSTEM_ERROR, name = "SYSTEM_ERROR"), @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_OPTIMIZE_MEASURE, + equals = PRIVATE_FLAG_OPTIMIZE_MEASURE, + name = "OPTIMIZE_MEASURE"), + @ViewDebug.FlagToString( mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, name = "DISABLE_WALLPAPER_TOUCH_EVENTS"), diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index d067d4bc366b..497f0668107f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -66,8 +66,7 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; /** - * <p>The {@link ContentCaptureManager} provides additional ways for for apps to - * integrate with the content capture subsystem. + * <p>Provides additional ways for apps to integrate with the content capture subsystem. * * <p>Content capture provides real-time, continuous capture of application activity, display and * events to an intelligence service that is provided by the Android system. The intelligence diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java index 11f1b6f17566..4c874892d576 100644 --- a/core/java/android/webkit/WebResourceError.java +++ b/core/java/android/webkit/WebResourceError.java @@ -19,7 +19,7 @@ package android.webkit; import android.annotation.SystemApi; /** - * Encapsulates information about errors occured during loading of web resources. See + * Encapsulates information about errors that occurred during loading of web resources. See * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)} */ public abstract class WebResourceError { diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java index 1024e2e50c3e..4a4f561c71ed 100644 --- a/core/java/android/window/BackEvent.java +++ b/core/java/android/window/BackEvent.java @@ -18,10 +18,8 @@ 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; @@ -52,8 +50,6 @@ public class BackEvent implements Parcelable { @SwipeEdge private final int mSwipeEdge; - @Nullable - private final RemoteAnimationTarget mDepartingAnimationTarget; /** * Creates a new {@link BackEvent} instance. @@ -62,16 +58,12 @@ 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, - @Nullable RemoteAnimationTarget departingAnimationTarget) { + public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) { mTouchX = touchX; mTouchY = touchY; mProgress = progress; mSwipeEdge = swipeEdge; - mDepartingAnimationTarget = departingAnimationTarget; } private BackEvent(@NonNull Parcel in) { @@ -79,7 +71,6 @@ 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>() { @@ -105,7 +96,6 @@ public class BackEvent implements Parcelable { dest.writeFloat(mTouchY); dest.writeFloat(mProgress); dest.writeInt(mSwipeEdge); - dest.writeTypedObject(mDepartingAnimationTarget, flags); } /** @@ -136,16 +126,6 @@ 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{" @@ -153,7 +133,6 @@ public class BackEvent implements Parcelable { + ", mTouchY=" + mTouchY + ", mProgress=" + mProgress + ", mSwipeEdge" + mSwipeEdge - + ", mDepartingAnimationTarget" + mDepartingAnimationTarget + "}"; } } diff --git a/core/java/android/window/BackMotionEvent.aidl b/core/java/android/window/BackMotionEvent.aidl new file mode 100644 index 000000000000..7c675c35c073 --- /dev/null +++ b/core/java/android/window/BackMotionEvent.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.window; + +/** + * @hide + */ +parcelable BackMotionEvent; diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java new file mode 100644 index 000000000000..8012a1c26bac --- /dev/null +++ b/core/java/android/window/BackMotionEvent.java @@ -0,0 +1,150 @@ +/* + * 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.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.RemoteAnimationTarget; + +/** + * Object used to report back gesture progress. Holds information about a {@link BackEvent} plus + * any {@link RemoteAnimationTarget} the gesture manipulates. + * + * @see BackEvent + * @hide + */ +public final class BackMotionEvent implements Parcelable { + private final float mTouchX; + private final float mTouchY; + private final float mProgress; + + @BackEvent.SwipeEdge + private final int mSwipeEdge; + @Nullable + private final RemoteAnimationTarget mDepartingAnimationTarget; + + /** + * Creates a new {@link BackMotionEvent} instance. + * + * @param touchX Absolute X location of the touch point of this event. + * @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 BackMotionEvent(float touchX, float touchY, float progress, + @BackEvent.SwipeEdge int swipeEdge, + @Nullable RemoteAnimationTarget departingAnimationTarget) { + mTouchX = touchX; + mTouchY = touchY; + mProgress = progress; + mSwipeEdge = swipeEdge; + mDepartingAnimationTarget = departingAnimationTarget; + } + + private BackMotionEvent(@NonNull Parcel in) { + mTouchX = in.readFloat(); + mTouchY = in.readFloat(); + mProgress = in.readFloat(); + mSwipeEdge = in.readInt(); + mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR); + } + + @NonNull + public static final Creator<BackMotionEvent> CREATOR = new Creator<BackMotionEvent>() { + @Override + public BackMotionEvent createFromParcel(Parcel in) { + return new BackMotionEvent(in); + } + + @Override + public BackMotionEvent[] newArray(int size) { + return new BackMotionEvent[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeFloat(mTouchX); + dest.writeFloat(mTouchY); + dest.writeFloat(mProgress); + dest.writeInt(mSwipeEdge); + dest.writeTypedObject(mDepartingAnimationTarget, flags); + } + + /** + * Returns the progress of a {@link BackEvent}. + * + * @see BackEvent#getProgress() + */ + @FloatRange(from = 0, to = 1) + public float getProgress() { + return mProgress; + } + + /** + * Returns the absolute X location of the touch point. + */ + public float getTouchX() { + return mTouchX; + } + + /** + * Returns the absolute Y location of the touch point. + */ + public float getTouchY() { + return mTouchY; + } + + /** + * Returns the screen edge that the swipe starts from. + */ + @BackEvent.SwipeEdge + public int getSwipeEdge() { + 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 "BackMotionEvent{" + + "mTouchX=" + mTouchX + + ", mTouchY=" + mTouchY + + ", mProgress=" + mProgress + + ", mSwipeEdge" + mSwipeEdge + + ", mDepartingAnimationTarget" + mDepartingAnimationTarget + + "}"; + } +} diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java index dd4385c8f50c..38c52e7473f1 100644 --- a/core/java/android/window/BackProgressAnimator.java +++ b/core/java/android/window/BackProgressAnimator.java @@ -40,7 +40,7 @@ public class BackProgressAnimator { private final SpringAnimation mSpring; private ProgressCallback mCallback; private float mProgress = 0; - private BackEvent mLastBackEvent; + private BackMotionEvent mLastBackEvent; private boolean mStarted = false; private void setProgress(float progress) { @@ -82,9 +82,9 @@ public class BackProgressAnimator { /** * Sets a new target position for the back progress. * - * @param event the {@link BackEvent} containing the latest target progress. + * @param event the {@link BackMotionEvent} containing the latest target progress. */ - public void onBackProgressed(BackEvent event) { + public void onBackProgressed(BackMotionEvent event) { if (!mStarted) { return; } @@ -95,11 +95,11 @@ public class BackProgressAnimator { /** * Starts the back progress animation. * - * @param event the {@link BackEvent} that started the gesture. + * @param event the {@link BackMotionEvent} 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) { + public void onBackStarted(BackMotionEvent event, ProgressCallback callback) { reset(); mLastBackEvent = event; mCallback = callback; @@ -129,8 +129,7 @@ public class BackProgressAnimator { } mCallback.onProgressUpdate( new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(), - progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(), - mLastBackEvent.getDepartingAnimationTarget())); + progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge())); } } diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl index 6af8ddda3a62..159c0e8afed0 100644 --- a/core/java/android/window/IOnBackInvokedCallback.aidl +++ b/core/java/android/window/IOnBackInvokedCallback.aidl @@ -17,7 +17,7 @@ package android.window; -import android.window.BackEvent; +import android.window.BackMotionEvent; /** * Interface that wraps a {@link OnBackInvokedCallback} object, to be stored in window manager @@ -30,18 +30,19 @@ oneway interface IOnBackInvokedCallback { * Called when a back gesture has been started, or back button has been pressed down. * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}. * - * @param backEvent The {@link BackEvent} containing information about the touch or button press. + * @param backMotionEvent The {@link BackMotionEvent} containing information about the touch + * or button press. */ - void onBackStarted(in BackEvent backEvent); + void onBackStarted(in BackMotionEvent backMotionEvent); /** * Called on back gesture progress. * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}. * - * @param backEvent The {@link BackEvent} containing information about the latest touch point - * and the progress that the back animation should seek to. + * @param backMotionEvent The {@link BackMotionEvent} containing information about the latest + * touch point and the progress that the back animation should seek to. */ - void onBackProgressed(in BackEvent backEvent); + void onBackProgressed(in BackMotionEvent backMotionEvent); /** * Called when a back gesture or back button press has been cancelled. diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index e6bb1f64ad86..0032b9ce0512 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -40,7 +40,8 @@ interface ITaskOrganizerController { void unregisterTaskOrganizer(ITaskOrganizer organizer); /** Creates a persistent root task in WM for a particular windowing-mode. */ - void createRootTask(int displayId, int windowingMode, IBinder launchCookie); + void createRootTask(int displayId, int windowingMode, IBinder launchCookie, + boolean removeWithTaskOrganizer); /** Deletes a persistent root task in WM */ boolean deleteRootTask(in WindowContainerToken task); diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index c9ddf92d3740..203d79aad7a3 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -71,20 +71,42 @@ public final class TaskFragmentCreationParams implements Parcelable { * * This is needed in case we need to launch a placeholder Activity to split below a transparent * always-expand Activity. + * + * This should not be used with {@link #mPairedActivityToken}. */ @Nullable private final IBinder mPairedPrimaryFragmentToken; + /** + * The Activity token to place the new TaskFragment on top of. + * When it is set, the new TaskFragment will be positioned right above the target Activity. + * Otherwise, the new TaskFragment will be positioned on the top of the Task by default. + * + * This is needed in case we need to place an Activity into TaskFragment to launch placeholder + * below a transparent always-expand Activity, or when there is another Intent being started in + * a TaskFragment above. + * + * This should not be used with {@link #mPairedPrimaryFragmentToken}. + */ + @Nullable + private final IBinder mPairedActivityToken; + private TaskFragmentCreationParams( @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect initialBounds, - @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) { + @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken, + @Nullable IBinder pairedActivityToken) { + if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) { + throw new IllegalArgumentException("pairedPrimaryFragmentToken and" + + " pairedActivityToken should not be set at the same time."); + } mOrganizer = organizer; mFragmentToken = fragmentToken; mOwnerToken = ownerToken; mInitialBounds.set(initialBounds); mWindowingMode = windowingMode; mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken; + mPairedActivityToken = pairedActivityToken; } @NonNull @@ -121,6 +143,15 @@ public final class TaskFragmentCreationParams implements Parcelable { return mPairedPrimaryFragmentToken; } + /** + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @Nullable + public IBinder getPairedActivityToken() { + return mPairedActivityToken; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); @@ -128,6 +159,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mInitialBounds.readFromParcel(in); mWindowingMode = in.readInt(); mPairedPrimaryFragmentToken = in.readStrongBinder(); + mPairedActivityToken = in.readStrongBinder(); } /** @hide */ @@ -139,6 +171,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mInitialBounds.writeToParcel(dest, flags); dest.writeInt(mWindowingMode); dest.writeStrongBinder(mPairedPrimaryFragmentToken); + dest.writeStrongBinder(mPairedActivityToken); } @NonNull @@ -164,6 +197,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " initialBounds=" + mInitialBounds + " windowingMode=" + mWindowingMode + " pairedFragmentToken=" + mPairedPrimaryFragmentToken + + " pairedActivityToken=" + mPairedActivityToken + "}"; } @@ -194,6 +228,9 @@ public final class TaskFragmentCreationParams implements Parcelable { @Nullable private IBinder mPairedPrimaryFragmentToken; + @Nullable + private IBinder mPairedActivityToken; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -224,6 +261,8 @@ public final class TaskFragmentCreationParams implements Parcelable { * This is needed in case we need to launch a placeholder Activity to split below a * transparent always-expand Activity. * + * This should not be used with {@link #setPairedActivityToken}. + * * TODO(b/232476698): remove the hide with adding CTS for this in next release. * @hide */ @@ -233,11 +272,32 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets the Activity token to place the new TaskFragment on top of. + * When it is set, the new TaskFragment will be positioned right above the target Activity. + * Otherwise, the new TaskFragment will be positioned on the top of the Task by default. + * + * This is needed in case we need to place an Activity into TaskFragment to launch + * placeholder below a transparent always-expand Activity, or when there is another Intent + * being started in a TaskFragment above. + * + * This should not be used with {@link #setPairedPrimaryFragmentToken}. + * + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @NonNull + public Builder setPairedActivityToken(@Nullable IBinder activityToken) { + mPairedActivityToken = activityToken; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, - mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken); + mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken, + mPairedActivityToken); } } } diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index bffd4e437dfa..02878f8ae72b 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -152,17 +152,33 @@ public class TaskOrganizer extends WindowOrganizer { * @param windowingMode Windowing mode to put the root task in. * @param launchCookie Launch cookie to associate with the task so that is can be identified * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) - @Nullable - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { try { - mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie); + mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie, + removeWithTaskOrganizer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param launchCookie Launch cookie to associate with the task so that is can be identified + * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + @Nullable + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */); + } + /** Deletes a persistent root task in WM */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull WindowContainerToken task) { diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index fda39c14dac7..dd9483a9c759 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -229,19 +229,21 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } @Override - public void onBackStarted(BackEvent backEvent) { + public void onBackStarted(BackMotionEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { mProgressAnimator.onBackStarted(backEvent, event -> callback.onBackProgressed(event)); - callback.onBackStarted(backEvent); + callback.onBackStarted(new BackEvent( + backEvent.getTouchX(), backEvent.getTouchY(), + backEvent.getProgress(), backEvent.getSwipeEdge())); } }); } @Override - public void onBackProgressed(BackEvent backEvent) { + public void onBackProgressed(BackMotionEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 1fcfe7dd5b6f..011232fe1915 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2953,12 +2953,24 @@ public class ChooserActivity extends ResolverActivity implements private boolean shouldShowStickyContentPreviewNoOrientationCheck() { return shouldShowTabs() - && mMultiProfilePagerAdapter.getListAdapterForUserHandle( - UserHandle.of(UserHandle.myUserId())).getCount() > 0 + && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( + UserHandle.of(UserHandle.myUserId())).getCount() > 0 + || shouldShowContentPreviewWhenEmpty()) && shouldShowContentPreview(); } /** + * This method could be used to override the default behavior when we hide the preview area + * when the current tab doesn't have any items. + * + * @return true if we want to show the content preview area even if the tab for the current + * user is empty + */ + protected boolean shouldShowContentPreviewWhenEmpty() { + return false; + } + + /** * @return true if we want to show the content preview area */ protected boolean shouldShowContentPreview() { diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index f8b764be582b..19e4ba405feb 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -209,7 +209,7 @@ public class ResolverActivity extends Activity implements * <p>Can only be used if there is a work profile. * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. */ - static final String EXTRA_SELECTED_PROFILE = + protected static final String EXTRA_SELECTED_PROFILE = "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE"; /** @@ -224,8 +224,8 @@ public class ResolverActivity extends Activity implements static final String EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; - static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; - static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; + protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; + protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; private BroadcastReceiver mWorkProfileStateReceiver; private UserHandle mHeaderCreatorUser; diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 017bf3f3dd6c..04fc4a6fd81d 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -338,4 +338,11 @@ oneway interface IStatusBar * @param leftOrTop indicates where the stage split is. */ void enterStageSplitFromRunningApp(boolean leftOrTop); + + /** + * Shows the media output switcher dialog. + * + * @param packageName of the session for which the output switcher is shown. + */ + void showMediaOutputSwitcher(String packageName); } diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java index 869da1ffcb6e..058c6ec4d13c 100644 --- a/core/java/com/android/internal/view/RotationPolicy.java +++ b/core/java/com/android/internal/view/RotationPolicy.java @@ -106,7 +106,9 @@ public final class RotationPolicy { * Enables or disables rotation lock from the system UI toggle. */ public static void setRotationLock(Context context, final boolean enabled) { - final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION; + final int rotation = areAllRotationsAllowed(context) + || useCurrentRotationOnRotationLockChange(context) ? CURRENT_ROTATION + : NATURAL_ROTATION; setRotationLockAtAngle(context, enabled, rotation); } @@ -139,6 +141,11 @@ public final class RotationPolicy { return context.getResources().getBoolean(R.bool.config_allowAllRotations); } + private static boolean useCurrentRotationOnRotationLockChange(Context context) { + return context.getResources().getBoolean( + R.bool.config_useCurrentRotationOnRotationLockChange); + } + private static void setRotationLock(final boolean enabled, final int rotation) { AsyncTask.execute(new Runnable() { @Override diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml index c382a6577857..2e65800d5abb 100644 --- a/core/res/res/layout/autofill_fill_dialog.xml +++ b/core/res/res/layout/autofill_fill_dialog.xml @@ -93,7 +93,7 @@ android:layout_height="36dp" android:layout_marginTop="6dp" android:layout_marginBottom="6dp" - style="@style/AutofillHalfSheetOutlinedButton" + style="?android:attr/borderlessButtonStyle" android:text="@string/autofill_save_no"> </Button> diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index fd08241deb3a..3c0b789dbe6f 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -81,7 +81,7 @@ android:layout_height="36dp" android:layout_marginTop="6dp" android:layout_marginBottom="6dp" - style="@style/AutofillHalfSheetOutlinedButton" + style="?android:attr/borderlessButtonStyle" android:text="@string/autofill_save_no"> </Button> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index cdfde3ef3042..e7bc695b2564 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -560,6 +560,10 @@ rotations as the default behavior. --> <bool name="config_allowAllRotations">false</bool> + <!-- If false and config_allowAllRotations is false, the screen will rotate to the natural + orientation of the device when the auto-rotate policy is toggled. --> + <bool name="config_useCurrentRotationOnRotationLockChange">false</bool> + <!-- If true, the direction rotation is applied to get to an application's requested orientation is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a portrait device an app requesting @@ -4919,9 +4923,8 @@ <!-- If face auth sends the user directly to home/last open app, or stays on keyguard --> <bool name="config_faceAuthDismissesKeyguard">true</bool> - <!-- Default value for whether a SFPS device is required to be interactive for fingerprint auth - to unlock the device. --> - <bool name="config_requireScreenOnToAuthEnabled">false</bool> + <!-- Default value for performant auth feature. --> + <bool name="config_performantAuthDefault">false</bool> <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3b9fcbe2f5b6..ccccbfa99d0d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1721,6 +1721,7 @@ <java-symbol type="attr" name="dialogTitleDecorLayout" /> <java-symbol type="attr" name="dialogTitleIconsDecorLayout" /> <java-symbol type="bool" name="config_allowAllRotations" /> + <java-symbol type="bool" name="config_useCurrentRotationOnRotationLockChange"/> <java-symbol type="bool" name="config_annoy_dianne" /> <java-symbol type="bool" name="config_startDreamImmediatelyOnDock" /> <java-symbol type="bool" name="config_carDockEnablesAccelerometer" /> @@ -2712,7 +2713,7 @@ <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" /> <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" /> <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" /> - <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" /> + <java-symbol type="bool" name="config_performantAuthDefault" /> <!-- Face config --> <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" /> diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index ed2b10117308..3768063f2a91 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -368,4 +368,20 @@ public class PropertyInvalidatedCacheTests { PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState"); assertEquals(n1, "cache_key.bluetooth.get_state"); } + + @Test + public void testOnTrimMemory() { + TestCache cache = new TestCache(MODULE, "trimMemoryTest"); + // The cache is not active until it has been invalidated once. + cache.invalidateCache(); + // Populate the cache with six entries. + for (int i = 0; i < 6; i++) { + cache.query(i); + } + // The maximum number of entries in TestCache is 4, so even though six entries were + // created, only four are retained. + assertEquals(4, cache.size()); + PropertyInvalidatedCache.onTrimMemory(); + assertEquals(0, cache.size()); + } } diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index f370ebd94545..9d6b29e5c072 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -17,6 +17,7 @@ package android.window; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -60,8 +61,8 @@ public class WindowOnBackInvokedDispatcherTest { private OnBackAnimationCallback mCallback1; @Mock private OnBackAnimationCallback mCallback2; - @Mock - private BackEvent mBackEvent; + private final BackMotionEvent mBackEvent = new BackMotionEvent( + 0, 0, 0, BackEvent.EDGE_LEFT, null); @Before public void setUp() throws Exception { @@ -89,12 +90,12 @@ public class WindowOnBackInvokedDispatcherTest { captor.capture()); captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(mBackEvent); + verify(mCallback1).onBackStarted(any(BackEvent.class)); verifyZeroInteractions(mCallback2); captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(mBackEvent); + verify(mCallback2).onBackStarted(any(BackEvent.class)); verifyNoMoreInteractions(mCallback1); } @@ -114,7 +115,7 @@ public class WindowOnBackInvokedDispatcherTest { assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY); captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(mBackEvent); + verify(mCallback1).onBackStarted(any(BackEvent.class)); } @Test @@ -152,6 +153,6 @@ public class WindowOnBackInvokedDispatcherTest { verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture()); captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(mBackEvent); + verify(mCallback2).onBackStarted(any(BackEvent.class)); } } diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp index dc4b5636a246..a5b192cd7ceb 100644 --- a/libs/WindowManager/Jetpack/Android.bp +++ b/libs/WindowManager/Jetpack/Android.bp @@ -63,6 +63,12 @@ android_library_import { sdk_version: "current", } +android_library_import { + name: "window-extensions-core", + aars: ["window-extensions-core-release.aar"], + sdk_version: "current", +} + java_library { name: "androidx.window.extensions", srcs: [ @@ -70,7 +76,10 @@ java_library { "src/androidx/window/util/**/*.java", "src/androidx/window/common/**/*.java", ], - static_libs: ["window-extensions"], + static_libs: [ + "window-extensions", + "window-extensions-core", + ], installable: true, sdk_version: "core_platform", system_ext_specific: true, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 54edd9ec4335..666b472c3716 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -48,7 +48,7 @@ public class WindowExtensionsImpl implements WindowExtensions { // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 1; + return 2; } @NonNull diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 87fa63d7fe14..00e13c94ea90 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -191,10 +191,25 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { */ void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { + createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode, + null /* pairedActivityToken */); + } + + /** + * @param ownerToken The token of the activity that creates this task fragment. It does not + * have to be a child of this task fragment, but must belong to the same task. + * @param pairedActivityToken The token of the activity that will be reparented to this task + * fragment. When it is not {@code null}, the task fragment will be + * positioned right above it. + */ + void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode, + @Nullable IBinder pairedActivityToken) { final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( getOrganizerToken(), fragmentToken, ownerToken) .setInitialBounds(bounds) .setWindowingMode(windowingMode) + .setPairedActivityToken(pairedActivityToken) .build(); createTaskFragment(wct, fragmentOptions); } @@ -216,8 +231,10 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode, @NonNull Activity activity) { - createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); - wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); + final IBinder reparentActivityToken = activity.getActivityToken(); + createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode, + reparentActivityToken); + wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken); } void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 1cd3ea5592e3..8b3a471ea306 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -77,6 +77,9 @@ import androidx.annotation.Nullable; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.extensions.WindowExtensionsImpl; +import androidx.window.extensions.core.util.function.Consumer; +import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; import androidx.window.extensions.layout.WindowLayoutComponentImpl; @@ -87,7 +90,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Main controller class that manages split states and presentation. @@ -113,7 +115,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * A developer-defined {@link SplitAttributes} calculator to compute the current * {@link SplitAttributes} with the current device and window states. - * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)} + * It is registered via {@link #setSplitAttributesCalculator(Function)} * and unregistered via {@link #clearSplitAttributesCalculator()}. * This is called when: * <ul> @@ -126,7 +128,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") @Nullable - private SplitAttributesCalculator mSplitAttributesCalculator; + private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator; /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info @@ -139,6 +141,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); /** Callback to Jetpack to notify about changes to split states. */ + @GuardedBy("mLock") @Nullable private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); @@ -164,7 +167,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen foldingFeatureProducer.addDataChangedCallback(new FoldingFeatureListener()); } - private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> { + private class FoldingFeatureListener + implements java.util.function.Consumer<List<CommonFoldingFeature>> { @Override public void accept(List<CommonFoldingFeature> foldingFeatures) { synchronized (mLock) { @@ -205,7 +209,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override - public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) { + public void setSplitAttributesCalculator( + @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator) { synchronized (mLock) { mSplitAttributesCalculator = calculator; } @@ -220,7 +225,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") @Nullable - SplitAttributesCalculator getSplitAttributesCalculator() { + Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() { return mSplitAttributesCalculator; } @@ -233,9 +238,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Registers the split organizer callback to notify about changes to active splits. + * @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with + * {@link WindowExtensionsImpl#getVendorApiLevel()} 2. */ + @Deprecated @Override - public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { + public void setSplitInfoCallback( + @NonNull java.util.function.Consumer<List<SplitInfo>> callback) { + Consumer<List<SplitInfo>> oemConsumer = callback::accept; + setSplitInfoCallback(oemConsumer); + } + + /** + * Registers the split organizer callback to notify about changes to active splits. + * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2 + */ + public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) { synchronized (mLock) { mEmbeddingCallback = callback; updateCallbackIfNecessary(); @@ -1481,7 +1499,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the active split that has the provided containers as primary and secondary or as * secondary and primary, if available. */ - @VisibleForTesting + @GuardedBy("mLock") @Nullable SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 14d244bbb6ce..668a7d5aa9b6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -43,11 +43,11 @@ import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; -import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams; import androidx.window.extensions.embedding.TaskContainer.TaskProperties; import androidx.window.extensions.layout.DisplayFeature; import androidx.window.extensions.layout.FoldingFeature; @@ -268,10 +268,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { container = mController.newContainer(activity, taskId); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(bounds); - createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(), - bounds, windowingMode); + final IBinder reparentActivityToken = activity.getActivityToken(); + createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken, + bounds, windowingMode, reparentActivityToken); wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), - activity.getActivityToken()); + reparentActivityToken); } else { resizeTaskFragmentIfRegistered(wct, container, bounds); final int windowingMode = mController.getTaskContainer(taskId) @@ -551,11 +552,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) { final Configuration taskConfiguration = taskProperties.getConfiguration(); final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration); - final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator(); + final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = + mController.getSplitAttributesCalculator(); final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); - final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); if (calculator == null) { - if (!isDefaultMinSizeSatisfied) { + if (!areDefaultConstraintsSatisfied) { return EXPAND_CONTAINERS_ATTRIBUTES; } return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, @@ -565,9 +567,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), taskConfiguration.windowConfiguration); final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( - taskWindowMetrics, taskConfiguration, defaultSplitAttributes, - isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag()); - final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params); + taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, + areDefaultConstraintsSatisfied, rule.getTag()); + final SplitAttributes splitAttributes = calculator.apply(params); return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 076856c373d6..17814c65e791 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -141,12 +141,26 @@ class TaskFragmentContainer { mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; if (pairedPrimaryContainer != null) { + // The TaskFragment will be positioned right above the paired container. if (pairedPrimaryContainer.getTaskContainer() != taskContainer) { throw new IllegalArgumentException( "pairedPrimaryContainer must be in the same Task"); } final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer); taskContainer.mContainers.add(primaryIndex + 1, this); + } else if (pendingAppearedActivity != null) { + // The TaskFragment will be positioned right above the pending appeared Activity. If any + // existing TaskFragment is empty with pending Intent, it is likely that the Activity of + // the pending Intent hasn't been created yet, so the new Activity should be below the + // empty TaskFragment. + int i = taskContainer.mContainers.size() - 1; + for (; i >= 0; i--) { + final TaskFragmentContainer container = taskContainer.mContainers.get(i); + if (!container.isEmpty() || container.getPendingAppearedIntent() == null) { + break; + } + } + taskContainer.mContainers.add(i + 1, this); } else { taskContainer.mContainers.add(this); } @@ -500,6 +514,8 @@ class TaskFragmentContainer { } if (!shouldFinishDependent) { + // Always finish the placeholder when the primary is finished. + finishPlaceholderIfAny(wct, presenter); return; } @@ -526,6 +542,28 @@ class TaskFragmentContainer { mActivitiesToFinishOnExit.clear(); } + @GuardedBy("mController.mLock") + private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct, + @NonNull SplitPresenter presenter) { + final List<TaskFragmentContainer> containersToRemove = new ArrayList<>(); + for (TaskFragmentContainer container : mContainersToFinishOnExit) { + if (container.mIsFinished) { + continue; + } + final SplitContainer splitContainer = mController.getActiveSplitForContainers( + this, container); + if (splitContainer != null && splitContainer.isPlaceholderContainer() + && splitContainer.getSecondaryContainer() == container) { + // Remove the placeholder secondary TaskFragment. + containersToRemove.add(container); + } + } + mContainersToFinishOnExit.removeAll(containersToRemove); + for (TaskFragmentContainer container : containersToRemove) { + container.finish(false /* shouldFinishDependent */, presenter, wct, mController); + } + } + boolean isFinished() { return mIsFinished; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index c9f870005eb9..8386131b177d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -45,6 +45,7 @@ import androidx.annotation.UiContext; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.extensions.core.util.function.Consumer; import androidx.window.util.DataProducer; import java.util.ArrayList; @@ -53,7 +54,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; /** * Reference implementation of androidx.window.extensions.layout OEM interface for use with @@ -82,6 +82,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners = new ArrayMap<>(); + @GuardedBy("mLock") + private final Map<java.util.function.Consumer<WindowLayoutInfo>, Consumer<WindowLayoutInfo>> + mJavaToExtConsumers = new ArrayMap<>(); + private final TaskFragmentOrganizer mTaskFragmentOrganizer; public WindowLayoutComponentImpl(@NonNull Context context, @@ -95,7 +99,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } /** Registers to listen to {@link CommonFoldingFeature} changes */ - public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) { + public void addFoldingStateChangedCallback( + java.util.function.Consumer<List<CommonFoldingFeature>> consumer) { synchronized (mLock) { mFoldingFeatureProducer.addDataChangedCallback(consumer); } @@ -109,13 +114,27 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { */ @Override public void addWindowLayoutInfoListener(@NonNull Activity activity, - @NonNull Consumer<WindowLayoutInfo> consumer) { - addWindowLayoutInfoListener((Context) activity, consumer); + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer = consumer::accept; + synchronized (mLock) { + mJavaToExtConsumers.put(consumer, extConsumer); + } + addWindowLayoutInfoListener(activity, extConsumer); + } + + @Override + public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer = consumer::accept; + synchronized (mLock) { + mJavaToExtConsumers.put(consumer, extConsumer); + } + addWindowLayoutInfoListener(context, extConsumer); } /** - * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context - * as a parameter. + * Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but + * takes a UI Context as a parameter. * * Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all * consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo} @@ -156,6 +175,18 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } + @Override + public void removeWindowLayoutInfoListener( + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer; + synchronized (mLock) { + extConsumer = mJavaToExtConsumers.remove(consumer); + } + if (extConsumer != null) { + removeWindowLayoutInfoListener(extConsumer); + } + } + /** * Removes a listener no longer interested in receiving updates. * diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index 2f92a577baa2..459ec9f89c4a 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -34,9 +34,11 @@ import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Pair; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; +import androidx.window.extensions.core.util.function.Predicate; import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.layout.DisplayFeature; import androidx.window.extensions.layout.FoldingFeature; @@ -107,7 +109,7 @@ public class EmbeddingTestUtils { static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); - return new SplitPairRule.Builder( + return createSplitPairRuleBuilder( activityPair -> false, targetPair::equals, w -> true) @@ -144,7 +146,7 @@ public class EmbeddingTestUtils { @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, int finishSecondaryWithPrimary, boolean clearTop) { final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); - return new SplitPairRule.Builder( + return createSplitPairRuleBuilder( targetPair::equals, activityIntentPair -> false, w -> true) @@ -223,4 +225,26 @@ public class EmbeddingTestUtils { displayFeatures.add(foldingFeature); return new WindowLayoutInfo(displayFeatures); } + + static ActivityRule.Builder createActivityBuilder( + @NonNull Predicate<Activity> activityPredicate, + @NonNull Predicate<Intent> intentPredicate) { + return new ActivityRule.Builder(activityPredicate, intentPredicate); + } + + static SplitPairRule.Builder createSplitPairRuleBuilder( + @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, + @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, + @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { + return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate, + windowMetricsPredicate); + } + + static SplitPlaceholderRule.Builder createSplitPlaceholderRuleBuilder( + @NonNull Intent placeholderIntent, @NonNull Predicate<Activity> activityPredicate, + @NonNull Predicate<Intent> intentPredicate, + @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { + return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate, + intentPredicate, windowMetricsPredicate); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 81c39571bffa..0bf0bc85b511 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -34,8 +34,11 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTR import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; @@ -432,7 +435,7 @@ public class SplitControllerTest { @Test public void testResolveStartActivityIntent_withoutLaunchingActivity() { final Intent intent = new Intent(); - final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + final ActivityRule expandRule = createActivityBuilder(r -> false, i -> i == intent) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -1170,7 +1173,7 @@ public class SplitControllerTest { @Test public void testHasSamePresentation() { - SplitPairRule splitRule1 = new SplitPairRule.Builder( + SplitPairRule splitRule1 = createSplitPairRuleBuilder( activityPair -> true, activityIntentPair -> true, windowMetrics -> true) @@ -1178,7 +1181,7 @@ public class SplitControllerTest { .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); - SplitPairRule splitRule2 = new SplitPairRule.Builder( + SplitPairRule splitRule2 = createSplitPairRuleBuilder( activityPair -> true, activityIntentPair -> true, windowMetrics -> true) @@ -1191,7 +1194,7 @@ public class SplitControllerTest { SplitController.haveSamePresentation(splitRule1, splitRule2, new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); - splitRule2 = new SplitPairRule.Builder( + splitRule2 = createSplitPairRuleBuilder( activityPair -> true, activityIntentPair -> true, windowMetrics -> true) @@ -1355,7 +1358,7 @@ public class SplitControllerTest { /** Setups a rule to always expand the given intent. */ private void setupExpandRule(@NonNull Intent expandIntent) { - final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals) + final ActivityRule expandRule = createActivityBuilder(r -> false, expandIntent::equals) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -1363,7 +1366,7 @@ public class SplitControllerTest { /** Setups a rule to always expand the given activity. */ private void setupExpandRule(@NonNull Activity expandActivity) { - final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false) + final ActivityRule expandRule = createActivityBuilder(expandActivity::equals, i -> false) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -1371,7 +1374,7 @@ public class SplitControllerTest { /** Setups a rule to launch placeholder for the given activity. */ private void setupPlaceholderRule(@NonNull Activity primaryActivity) { - final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 121e81394b2d..a288fd6a3067 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -28,6 +28,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUND import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; @@ -76,6 +77,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; @@ -511,7 +513,7 @@ public class SplitPresenterTest { final Activity secondaryActivity = createMockActivity(); final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID); final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); - final SplitPairRule rule = new SplitPairRule.Builder(pair -> + final SplitPairRule rule = createSplitPairRuleBuilder(pair -> pair.first == mActivity && pair.second == secondaryActivity, pair -> false, metrics -> true) .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) @@ -529,7 +531,7 @@ public class SplitPresenterTest { @Test public void testComputeSplitAttributes() { - final SplitPairRule splitPairRule = new SplitPairRule.Builder( + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( activityPair -> true, activityIntentPair -> true, windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) @@ -561,10 +563,10 @@ public class SplitPresenterTest { SplitAttributes.SplitType.RatioSplitType.splitEqually() ) ).build(); + final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = + params -> splitAttributes; - mController.setSplitAttributesCalculator(params -> { - return splitAttributes; - }); + mController.setSplitAttributesCalculator(calculator); assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties, splitPairRule, null /* minDimensionsPair */)); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 7d9d8b0f3a06..78b85e642c13 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -154,17 +154,52 @@ public class TaskFragmentContainerTest { null /* pendingAppearedIntent */, taskContainer, mController, null /* pairedPrimaryContainer */); doReturn(container1).when(mController).getContainerWithActivity(mActivity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); // The activity is requested to be reparented, so don't finish it. - container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container0.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); verify(mTransaction, never()).finishActivity(any()); - verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); + verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken()); verify(mController).removeContainer(container0); } @Test + public void testFinish_alwaysFinishPlaceholder() { + // Register container1 as a placeholder + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); + final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity); + container0.setInfo(mTransaction, info0); + final Activity placeholderActivity = createMockActivity(); + final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity, + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); + final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity); + container1.setInfo(mTransaction, info1); + final SplitAttributes splitAttributes = new SplitAttributes.Builder().build(); + final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(new Intent(), + mActivity::equals, (java.util.function.Predicate) i -> false, + (java.util.function.Predicate) w -> true) + .setDefaultSplitAttributes(splitAttributes) + .build(); + mController.registerSplit(mTransaction, container0, mActivity, container1, rule, + splitAttributes); + + // The placeholder TaskFragment should be finished even if the primary is finished with + // shouldFinishDependent = false. + container0.finish(false /* shouldFinishDependent */, mPresenter, mTransaction, mController); + + assertTrue(container0.isFinished()); + assertTrue(container1.isFinished()); + verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken()); + verify(mPresenter).deleteTaskFragment(mTransaction, container1.getTaskFragmentToken()); + verify(mController).removeContainer(container0); + verify(mController).removeContainer(container1); + } + + @Test public void testSetInfo() { final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. @@ -493,8 +528,6 @@ public class TaskFragmentContainerTest { final TaskFragmentContainer tf1 = new TaskFragmentContainer( null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, null /* pairedPrimaryTaskFragment */); - taskContainer.mContainers.add(tf0); - taskContainer.mContainers.add(tf1); // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted // right above tf0. @@ -506,6 +539,26 @@ public class TaskFragmentContainerTest { } @Test + public void testNewContainerWithPairedPendingAppearedActivity() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer tf0 = new TaskFragmentContainer( + createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + + // When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any + // TaskFragment without any Activity. + final TaskFragmentContainer tf2 = new TaskFragmentContainer( + createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + assertEquals(0, taskContainer.indexOf(tf0)); + assertEquals(1, taskContainer.indexOf(tf2)); + assertEquals(2, taskContainer.indexOf(tf1)); + } + + @Test public void testIsVisible() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer( diff --git a/libs/WindowManager/Jetpack/window-extensions-core-release.aar b/libs/WindowManager/Jetpack/window-extensions-core-release.aar Binary files differnew file mode 100644 index 000000000000..96ff840b984b --- /dev/null +++ b/libs/WindowManager/Jetpack/window-extensions-core-release.aar diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex 84ab4487feee..cddbf469b3c7 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml new file mode 100644 index 000000000000..6114ad6e277a --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape android:shape="rectangle" + android:tintMode="multiply" + android:tint="@color/decor_title_color" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/white" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml index 0bcaa530dc80..91edbf1a7bd4 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml @@ -14,11 +14,11 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" + android:tint="@color/decor_button_dark_color"> <path android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/> </vector> diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decor.xml b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml new file mode 100644 index 000000000000..f3d219872001 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:background="@drawable/caption_decor_title"> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/back_button" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/back_button_text" + android:background="@drawable/decor_back_button_dark" + android:duplicateParentState="true"/> + <Space + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="1" + android:elevation="2dp"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/minimize_window" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/minimize_button_text" + android:background="@drawable/decor_minimize_button_dark" + android:duplicateParentState="true"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/maximize_window" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/maximize_button_text" + android:background="@drawable/decor_maximize_button_dark" + android:duplicateParentState="true"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/close_window" + android:contentDescription="@string/close_button_text" + android:background="@drawable/decor_close_button_dark" + android:duplicateParentState="true"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index e58e785850fa..97a9fede22d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -256,12 +256,30 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + */ public void createRootTask(int displayId, int windowingMode, TaskListener listener) { - ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */); + } + + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + */ + public void createRootTask(int displayId, int windowingMode, TaskListener listener, + boolean removeWithTaskOrganizer) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" , displayId, windowingMode, listener.toString()); final IBinder cookie = new Binder(); setPendingLaunchCookieListener(cookie, listener); - super.createRootTask(displayId, windowingMode, cookie); + super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer); } /** 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 cbcd9498fe55..236309207b4f 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 @@ -51,6 +51,7 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.BackAnimationAdaptor; import android.window.BackEvent; +import android.window.BackMotionEvent; import android.window.BackNavigationInfo; import android.window.IBackAnimationRunner; import android.window.IBackNaviAnimationController; @@ -173,11 +174,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean consumed = false; if (mWaitingAnimation && mOnBackCallback != null) { if (mTriggerBack) { - final BackEvent backFinish = mTouchTracker.createProgressEvent(1); + final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(1); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackInvoked(mOnBackCallback); } else { - final BackEvent backFinish = mTouchTracker.createProgressEvent(0); + final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(0); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackCancelled(mOnBackCallback); } @@ -480,7 +481,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - final BackEvent backEvent = mTouchTracker.createProgressEvent(); + final BackMotionEvent backEvent = mTouchTracker.createProgressEvent(); if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) { dispatchOnBackProgressed(mBackToLauncherCallback, backEvent); } else if (mEnableAnimations.get()) { @@ -573,7 +574,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void dispatchOnBackStarted(IOnBackInvokedCallback callback, - BackEvent backEvent) { + BackMotionEvent backEvent) { if (callback == null) { return; } @@ -611,7 +612,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, - BackEvent backEvent) { + BackMotionEvent backEvent) { if (callback == null) { return; } @@ -730,7 +731,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } dispatchOnBackStarted(mBackToLauncherCallback, mTouchTracker.createStartEvent(mAnimationTarget)); - final BackEvent backInit = mTouchTracker.createProgressEvent(); + final BackMotionEvent backInit = mTouchTracker.createProgressEvent(); if (!mCachingBackDispatcher.consume()) { dispatchOnBackProgressed(mBackToLauncherCallback, backInit); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index ccfac65d6342..695ef4e66302 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -19,6 +19,7 @@ package com.android.wm.shell.back; import android.os.SystemProperties; import android.view.RemoteAnimationTarget; import android.window.BackEvent; +import android.window.BackMotionEvent; /** * Helper class to record the touch location for gesture and generate back events. @@ -82,11 +83,11 @@ class TouchTracker { mSwipeEdge = BackEvent.EDGE_LEFT; } - BackEvent createStartEvent(RemoteAnimationTarget target) { - return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); + BackMotionEvent createStartEvent(RemoteAnimationTarget target) { + return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); } - BackEvent createProgressEvent() { + BackMotionEvent createProgressEvent() { float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; progressThreshold = progressThreshold == 0 ? 1 : progressThreshold; @@ -109,8 +110,8 @@ class TouchTracker { return createProgressEvent(progress); } - BackEvent createProgressEvent(float progress) { - return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); + BackMotionEvent createProgressEvent(float progress) { + return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); } public void setProgressThreshold(float progressThreshold) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index dd8afff0df2c..71e15c12b9c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -973,21 +973,59 @@ public class BubbleController implements ConfigurationChangeListener { } /** - * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n - * otification and remain until the user dismisses the bubble or bubble stack. Only one intent - * bubble is supported at a time. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. */ - public void showAppBubble(Intent intent) { - if (intent == null || intent.getPackage() == null) return; + public void showOrHideAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) { + Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + + ((intent != null) ? " with package: " + intent.getPackage() : " ")); + return; + } PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId); if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; - Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); - b.setShouldAutoExpand(true); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); + if (existingAppBubble != null) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (isStackExpanded()) { + if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) { + // App bubble is expanded, lets collapse + collapseStack(); + } else { + // App bubble is not selected, select it + mBubbleData.setSelectedBubble(existingAppBubble); + } + } else { + // App bubble is not selected, select it & expand + mBubbleData.setSelectedBubble(existingAppBubble); + mBubbleData.setExpanded(true); + } + } else { + // App bubble does not exist, lets add and expand it + Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } } /** @@ -1697,9 +1735,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void showAppBubble(Intent intent) { + public void showOrHideAppBubble(Intent intent) { mMainExecutor.execute(() -> { - BubbleController.this.showAppBubble(intent); + BubbleController.this.showOrHideAppBubble(intent); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index af31391fec96..6230d22ebe12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -684,7 +685,8 @@ public class BubbleData { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) + || KEY_APP_BUBBLE.equals(bubble.getKey())) { return; } if (DEBUG_BUBBLE_DATA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 465d1abe0a3d..df4325763a17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -109,13 +109,28 @@ public interface Bubbles { void expandStackAndSelectBubble(Bubble bubble); /** - * Adds and expands bubble that is not notification based, but instead based on an intent from - * the app. The intent must be explicit (i.e. include a package name or fully qualified - * component class name) and the activity for it should be resizable. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded * - * @param intent the intent to populate the bubble. + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. */ - void showAppBubble(Intent intent); + void showOrHideAppBubble(Intent intent); /** * @return a bubble that matches the provided shortcutId, if one exists. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index a9d3c9f154cd..abb357c5b653 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -78,6 +78,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private final Rect mResizingBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; + private ValueAnimator mScreenshotAnimator; private int mIconSize; private int mOffsetX; @@ -135,8 +136,17 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Releases the surfaces for split decor. */ public void release(SurfaceControl.Transaction t) { - if (mFadeAnimator != null && mFadeAnimator.isRunning()) { - mFadeAnimator.cancel(); + if (mFadeAnimator != null) { + if (mFadeAnimator.isRunning()) { + mFadeAnimator.cancel(); + } + mFadeAnimator = null; + } + if (mScreenshotAnimator != null) { + if (mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mScreenshotAnimator = null; } if (mViewHost != null) { mViewHost.release(); @@ -238,16 +248,20 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Stops showing resizing hint. */ public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { if (mScreenshot != null) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + t.setPosition(mScreenshot, mOffsetX, mOffsetY); final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); - final ValueAnimator va = ValueAnimator.ofFloat(1, 0); - va.addUpdateListener(valueAnimator -> { + mScreenshotAnimator = ValueAnimator.ofFloat(1, 0); + mScreenshotAnimator.addUpdateListener(valueAnimator -> { final float progress = (float) valueAnimator.getAnimatedValue(); animT.setAlpha(mScreenshot, progress); animT.apply(); }); - va.addListener(new AnimatorListenerAdapter() { + mScreenshotAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mRunningAnimationCount++; @@ -266,7 +280,7 @@ public class SplitDecorManager extends WindowlessWindowManager { } } }); - va.start(); + mScreenshotAnimator.start(); } if (mResizingIconView == null) { @@ -292,9 +306,6 @@ public class SplitDecorManager extends WindowlessWindowManager { }); return; } - - // If fade-in animation is running, cancel it and re-run fade-out one. - mFadeAnimator.cancel(); } if (mShown) { fadeOutDecor(animFinishedCallback); @@ -332,6 +343,11 @@ public class SplitDecorManager extends WindowlessWindowManager { * directly. */ public void fadeOutDecor(Runnable finishedCallback) { if (mShown) { + // If previous animation is running, just cancel it. + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + mFadeAnimator.cancel(); + } + startFadeAnimation(false /* show */, true, finishedCallback); mShown = false; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index d3b9fa5e628d..512a4ef386bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -49,6 +49,7 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; @@ -93,6 +94,7 @@ import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition; import com.android.wm.shell.unfold.qualifier.UnfoldTransition; +import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -192,7 +194,8 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController) { - return new DesktopModeWindowDecorViewModel( + if (DesktopModeStatus.isAnyEnabled()) { + return new DesktopModeWindowDecorViewModel( context, mainHandler, mainChoreographer, @@ -201,6 +204,14 @@ public abstract class WMShellModule { syncQueue, desktopModeController, desktopTasksController); + } + return new CaptionWindowDecorViewModel( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index f5f3573252ec..a839a230ea6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -36,6 +36,7 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.ArraySet; @@ -251,7 +252,8 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll * Show apps on desktop */ void showDesktopApps() { - WindowContainerTransaction wct = bringDesktopAppsToFront(); + // Bring apps to front, ignoring their visibility status to always ensure they are on top. + WindowContainerTransaction wct = bringDesktopAppsToFront(true /* ignoreVisibility */); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */); @@ -260,8 +262,13 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } } + /** Get number of tasks that are marked as visible */ + int getVisibleTaskCount() { + return mDesktopModeTaskRepository.getVisibleTaskCount(); + } + @NonNull - private WindowContainerTransaction bringDesktopAppsToFront() { + private WindowContainerTransaction bringDesktopAppsToFront(boolean force) { final WindowContainerTransaction wct = new WindowContainerTransaction(); final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(); ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size()); @@ -278,12 +285,14 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll return wct; } - final boolean allActiveTasksAreVisible = taskInfos.stream() - .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId)); - if (allActiveTasksAreVisible) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, - "bringDesktopAppsToFront: active tasks are already in front, skipping."); - return wct; + if (!force) { + final boolean allActiveTasksAreVisible = taskInfos.stream() + .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId)); + if (allActiveTasksAreVisible) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "bringDesktopAppsToFront: active tasks are already in front, skipping."); + return wct; + } } ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: reordering all active tasks to the front"); @@ -354,7 +363,7 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll if (wct == null) { wct = new WindowContainerTransaction(); } - wct.merge(bringDesktopAppsToFront(), true /* transfer */); + wct.merge(bringDesktopAppsToFront(false /* ignoreVisibility */), true /* transfer */); wct.reorder(request.getTriggerTask().token, true /* onTop */); return wct; @@ -435,5 +444,15 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll executeRemoteCallWithTaskPermission(mController, "showDesktopApps", DesktopModeController::showDesktopApps); } + + @Override + public int getVisibleTaskCount() throws RemoteException { + int[] result = new int[1]; + executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount", + controller -> result[0] = controller.getVisibleTaskCount(), + true /* blocking */ + ); + return result[0]; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 600ccc17ecaa..47342c9f21ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -143,6 +143,13 @@ class DesktopModeTaskRepository { } /** + * Get number of tasks that are marked as visible + */ + fun getVisibleTaskCount(): Int { + return visibleTasks.size + } + + /** * Add (or move if it already exists) the task to the top of the ordered list. */ fun addOrMoveFreeformTaskToTop(taskId: Int) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 3341470efe4d..23033253eaf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -84,8 +84,7 @@ class DesktopTasksController( fun showDesktopApps() { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps") val wct = WindowContainerTransaction() - - bringDesktopAppsToFront(wct) + bringDesktopAppsToFront(wct, force = true) // Execute transaction if there are pending operations if (!wct.isEmpty) { @@ -97,6 +96,11 @@ class DesktopTasksController( } } + /** Get number of tasks that are marked as visible */ + fun getVisibleTaskCount(): Int { + return desktopModeTaskRepository.getVisibleTaskCount() + } + /** Move a task with given `taskId` to desktop */ fun moveToDesktop(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) } @@ -150,11 +154,11 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } - private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) { + private fun bringDesktopAppsToFront(wct: WindowContainerTransaction, force: Boolean = false) { val activeTasks = desktopModeTaskRepository.getActiveTasks() // Skip if all tasks are already visible - if (activeTasks.isNotEmpty() && activeTasks.all(desktopModeTaskRepository::isVisibleTask)) { + if (!force && activeTasks.all(desktopModeTaskRepository::isVisibleTask)) { ProtoLog.d( WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: active tasks are already in front, skipping." @@ -310,5 +314,16 @@ class DesktopTasksController( Consumer(DesktopTasksController::showDesktopApps) ) } + + override fun getVisibleTaskCount(): Int { + val result = IntArray(1) + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "getVisibleTaskCount", + { controller -> result[0] = controller.getVisibleTaskCount() }, + true /* blocking */ + ) + return result[0] + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index 5042bd6f2d65..d0739e14675f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -23,4 +23,7 @@ interface IDesktopMode { /** Show apps on the desktop */ void showDesktopApps(); + + /** Get count of visible desktop tasks */ + int getVisibleTaskCount(); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index cd61dbb5b7d1..f6d67d858f98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -47,7 +47,7 @@ public class PipBoundsAlgorithm { private final @NonNull PipBoundsState mPipBoundsState; private final PipSnapAlgorithm mSnapAlgorithm; - private final PipKeepClearAlgorithm mPipKeepClearAlgorithm; + private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; private float mDefaultSizePercent; private float mMinAspectRatioForMinSize; @@ -62,7 +62,7 @@ public class PipBoundsAlgorithm { public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm, - @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) { + @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm) { mPipBoundsState = pipBoundsState; mSnapAlgorithm = pipSnapAlgorithm; mPipKeepClearAlgorithm = pipKeepClearAlgorithm; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java index e3495e100c62..5045cf905ee6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java @@ -24,7 +24,7 @@ import java.util.Set; * Interface for interacting with keep clear algorithm used to move PiP window out of the way of * keep clear areas. */ -public interface PipKeepClearAlgorithm { +public interface PipKeepClearAlgorithmInterface { /** * Adjust the position of picture in picture window based on the registered keep clear areas. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index e6c7e101d078..83158ffafa7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -662,8 +662,8 @@ public class PipTransition extends PipTransitionController { } // Please file a bug to handle the unexpected transition type. - throw new IllegalStateException("Entering PIP with unexpected transition type=" - + transitTypeToString(transitType)); + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java index 690505e03fce..ed8dc7ded654 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java @@ -26,14 +26,14 @@ import android.view.Gravity; import com.android.wm.shell.R; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; -import com.android.wm.shell.pip.PipKeepClearAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import java.util.Set; /** * Calculates the adjusted position that does not occlude keep clear areas. */ -public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm { +public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface { private boolean mKeepClearAreaGravityEnabled = SystemProperties.getBoolean( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 281ea530e9e1..431bd7b08142 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -333,6 +333,9 @@ public class PhonePipMenuController implements PipMenuController { mTmpDestinationRectF.set(destinationBounds); mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform); @@ -359,6 +362,9 @@ public class PhonePipMenuController implements PipMenuController { } final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setCrop(surfaceControl, destinationBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 01d81ff4e436..e83854e22fa2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -46,8 +46,6 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; -import android.os.UserHandle; -import android.os.UserManager; import android.util.Pair; import android.util.Size; import android.view.DisplayInfo; @@ -85,7 +83,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; -import com.android.wm.shell.pip.PipKeepClearAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -137,7 +135,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private PipAppOpsListener mAppOpsListener; private PipMediaController mMediaController; private PipBoundsAlgorithm mPipBoundsAlgorithm; - private PipKeepClearAlgorithm mPipKeepClearAlgorithm; + private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; private PipBoundsState mPipBoundsState; private PipMotionHelper mPipMotionHelper; private PipTouchHandler mTouchHandler; @@ -380,7 +378,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, + PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, @@ -419,7 +417,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, + PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, @NonNull PipBoundsState pipBoundsState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, @@ -814,7 +812,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - if (!mPipTaskOrganizer.isInPip()) { + if (!mPipTransitionState.hasEnteredPip()) { return; } if (visible) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 83bc7c0e6e7d..850c561c891f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -337,8 +337,10 @@ public class PipTouchHandler { mMotionHelper.synchronizePinnedStackBounds(); reloadResources(); - // Recreate the dismiss target for the new orientation. - mPipDismissTargetHandler.createOrUpdateDismissTarget(); + if (mPipTaskOrganizer.isInPip()) { + // Recreate the dismiss target for the new orientation. + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + } } public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index ce34d2f9547d..1ff77f7d36dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -39,7 +39,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.pip.PipBoundsAlgorithm; -import com.android.wm.shell.pip.PipKeepClearAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -65,7 +65,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { @NonNull TvPipBoundsState tvPipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm) { super(context, tvPipBoundsState, pipSnapAlgorithm, - new PipKeepClearAlgorithm() {}); + new PipKeepClearAlgorithmInterface() {}); this.mTvPipBoundsState = tvPipBoundsState; this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); reloadResources(context); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 8ddc3c04d991..a6c4ac28c1fd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -605,9 +605,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId2 == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.startTask(taskId1, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.startTask(taskId1, options1); - startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -632,9 +642,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -696,6 +716,34 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mShouldUpdateRecents = false; mIsSplitEntering = true; + setSideStagePosition(sidePosition, wct); + if (!mMainStage.isActive()) { + mMainStage.activate(wct, false /* reparent */); + } + + if (mainOptions == null) mainOptions = new Bundle(); + addActivityOptions(mainOptions, mMainStage); + mainOptions = wrapAsSplitRemoteAnimation(adapter, mainOptions); + + updateWindowBounds(mSplitLayout, wct); + if (mainTaskId == INVALID_TASK_ID) { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); + } else { + wct.startTask(mainTaskId, mainOptions); + } + + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + }); + + setEnterInstanceId(instanceId); + } + + private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); if (isSplitScreenVisible()) { mMainStage.evictAllChildren(evictWct); @@ -739,37 +787,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }; RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); - - if (mainOptions == null) { - mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); - } else { - ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); - mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - mainOptions = mainActivityOptions.toBundle(); - } - - setSideStagePosition(sidePosition, wct); - if (!mMainStage.isActive()) { - mMainStage.activate(wct, false /* reparent */); - } - - if (mainOptions == null) mainOptions = new Bundle(); - addActivityOptions(mainOptions, mMainStage); - updateWindowBounds(mSplitLayout, wct); - if (mainTaskId == INVALID_TASK_ID) { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); - } else { - wct.startTask(mainTaskId, mainOptions); - } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - setDividerVisibility(true, t); - }); - - setEnterInstanceId(instanceId); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + return activityOptions.toBundle(); } private void setEnterInstanceId(InstanceId instanceId) { @@ -1052,6 +1072,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); + wct.setForceTranslucent(mRootTaskInfo.token, true); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { @@ -1083,6 +1104,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); + finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(finishedWCT); mSyncQueue.runInSync(at -> { @@ -1228,8 +1250,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - private void addActivityOptions(Bundle opts, StageTaskListener stage) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + if (launchTarget != null) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token); + } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); @@ -1463,6 +1487,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private void onStageVisibilityChanged(StageListenerImpl stageListener) { + // If split didn't active, just ignore this callback because we should already did these + // on #applyExitSplitScreen. + if (!isSplitActive()) { + return; + } + final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; @@ -1471,20 +1501,23 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } + // Check if it needs to dismiss split screen when both stage invisible. + if (!mainStageVisible && mExitSplitScreenOnHide) { + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); if (!mainStageVisible) { + // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); wct.setForceTranslucent(mRootTaskInfo.token, true); - // Both stages are not visible, check if it needs to dismiss split screen. - if (mExitSplitScreenOnHide) { - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); - } } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, false); } + mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(mainStageVisible, t); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java new file mode 100644 index 000000000000..129924ad5d05 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.os.Handler; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; + +/** + * View model for the window decoration with a caption and shadows. Works with + * {@link CaptionWindowDecoration}. + */ +public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + private final ShellTaskOrganizer mTaskOrganizer; + private final Context mContext; + private final Handler mMainHandler; + private final Choreographer mMainChoreographer; + private final DisplayController mDisplayController; + private final SyncTransactionQueue mSyncQueue; + private TaskOperations mTaskOperations; + + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + + public CaptionWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue) { + mContext = context; + mMainHandler = mainHandler; + mMainChoreographer = mainChoreographer; + mTaskOrganizer = taskOrganizer; + mDisplayController = displayController; + mSyncQueue = syncQueue; + } + + @Override + public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { + mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); + } + + @Override + public boolean onTaskOpening( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (!shouldShowWindowDecor(taskInfo)) return false; + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + return true; + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + + if (decoration == null) return; + + decoration.relayout(taskInfo); + setupCaptionColor(taskInfo, decoration); + } + + @Override + public void onTaskChanging( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + + if (!shouldShowWindowDecor(taskInfo)) { + if (decoration != null) { + destroyWindowDecoration(taskInfo); + } + return; + } + + if (decoration == null) { + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + } else { + decoration.relayout(taskInfo, startT, finishT); + } + } + + @Override + public void onTaskClosing( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; + + decoration.relayout(taskInfo, startT, finishT); + } + + @Override + public void destroyWindowDecoration(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = + mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); + if (decoration == null) return; + + decoration.close(); + } + + private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { + final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + decoration.setCaptionColor(statusBarColor); + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { + return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode() + == WINDOWING_MODE_FREEFORM); + } + + private void createWindowDecoration( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (oldDecoration != null) { + // close the old decoration if it exists to avoid two window decorations being added + oldDecoration.close(); + } + final CaptionWindowDecoration windowDecoration = + new CaptionWindowDecoration( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + taskSurface, + mMainHandler, + mMainChoreographer, + mSyncQueue); + mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); + + final TaskPositioner taskPositioner = + new TaskPositioner(mTaskOrganizer, windowDecoration); + final CaptionTouchEventListener touchEventListener = + new CaptionTouchEventListener(taskInfo, taskPositioner); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); + windowDecoration.setDragResizeCallback(taskPositioner); + windowDecoration.relayout(taskInfo, startT, finishT); + setupCaptionColor(taskInfo, windowDecoration); + } + + private class CaptionTouchEventListener implements + View.OnClickListener, View.OnTouchListener { + + private final int mTaskId; + private final WindowContainerToken mTaskToken; + private final DragResizeCallback mDragResizeCallback; + + private int mDragPointerId = -1; + + private CaptionTouchEventListener( + RunningTaskInfo taskInfo, + DragResizeCallback dragResizeCallback) { + mTaskId = taskInfo.taskId; + mTaskToken = taskInfo.token; + mDragResizeCallback = dragResizeCallback; + } + + @Override + public void onClick(View v) { + final int id = v.getId(); + if (id == R.id.close_window) { + mTaskOperations.closeTask(mTaskToken); + } else if (id == R.id.back_button) { + mTaskOperations.injectBackKey(); + } else if (id == R.id.minimize_window) { + mTaskOperations.minimizeTask(mTaskToken); + } else if (id == R.id.maximize_window) { + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + mTaskOperations.maximizeTask(taskInfo); + } + } + + @Override + public boolean onTouch(View v, MotionEvent e) { + if (v.getId() != R.id.caption) { + return false; + } + handleEventForMove(e); + + if (e.getAction() != MotionEvent.ACTION_DOWN) { + return false; + } + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (taskInfo.isFocused) { + return false; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, true /* onTop */); + mSyncQueue.queue(wct); + return true; + } + + /** + * @param e {@link MotionEvent} to process + * @return {@code true} if a drag is happening; or {@code false} if it is not + */ + private void handleEventForMove(MotionEvent e) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + return; + } + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDragPointerId = e.getPointerId(0); + mDragResizeCallback.onDragResizeStart( + 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); + break; + } + case MotionEvent.ACTION_MOVE: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragResizeCallback.onDragResizeMove( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragResizeCallback.onDragResizeEnd( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + break; + } + } + } + } +} 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 new file mode 100644 index 000000000000..d26f1fc8ef1b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.VectorDrawable; +import android.os.Handler; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewConfiguration; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with + * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button, + * maximize button and close button. + */ +public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { + private final Handler mHandler; + private final Choreographer mChoreographer; + private final SyncTransactionQueue mSyncQueue; + + private View.OnClickListener mOnCaptionButtonClickListener; + private View.OnTouchListener mOnCaptionTouchListener; + private DragResizeCallback mDragResizeCallback; + private DragResizeInputListener mDragResizeListener; + private final DragDetector mDragDetector; + + private RelayoutParams mRelayoutParams = new RelayoutParams(); + private final RelayoutResult<WindowDecorLinearLayout> mResult = + new RelayoutResult<>(); + + CaptionWindowDecoration( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Handler handler, + Choreographer choreographer, + SyncTransactionQueue syncQueue) { + super(context, displayController, taskOrganizer, taskInfo, taskSurface); + + mHandler = handler; + mChoreographer = choreographer; + mSyncQueue = syncQueue; + mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop()); + } + + void setCaptionListeners( + View.OnClickListener onCaptionButtonClickListener, + View.OnTouchListener onCaptionTouchListener) { + mOnCaptionButtonClickListener = onCaptionButtonClickListener; + mOnCaptionTouchListener = onCaptionTouchListener; + } + + void setDragResizeCallback(DragResizeCallback dragResizeCallback) { + mDragResizeCallback = dragResizeCallback; + } + + @Override + void relayout(RunningTaskInfo taskInfo) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + relayout(taskInfo, t, t); + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + } + + void relayout(RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { + final int shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; + + final WindowDecorLinearLayout oldRootView = mResult.mRootView; + final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + final int outsetLeftId = R.dimen.freeform_resize_handle; + final int outsetTopId = R.dimen.freeform_resize_handle; + final int outsetRightId = R.dimen.freeform_resize_handle; + final int outsetBottomId = R.dimen.freeform_resize_handle; + + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo + + mTaskOrganizer.applyTransaction(wct); + + if (mResult.mRootView == null) { + // This means something blocks the window decor from showing, e.g. the task is hidden. + // Nothing is set up in this case including the decoration surface. + return; + } + if (oldRootView != mResult.mRootView) { + setupRootView(); + } + + if (!isDragResizeable) { + closeDragResizeListener(); + return; + } + + if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { + closeDragResizeListener(); + mDragResizeListener = new DragResizeInputListener( + mContext, + mHandler, + mChoreographer, + mDisplay.getDisplayId(), + mDecorationContainerSurface, + mDragResizeCallback); + } + + final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) + .getScaledTouchSlop(); + mDragDetector.setTouchSlop(touchSlop); + + final int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + final int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); + mDragResizeListener.setGeometry( + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); + } + + /** + * Sets up listeners when a new root view is created. + */ + private void setupRootView() { + final View caption = mResult.mRootView.findViewById(R.id.caption); + caption.setOnTouchListener(mOnCaptionTouchListener); + final View close = caption.findViewById(R.id.close_window); + close.setOnClickListener(mOnCaptionButtonClickListener); + final View back = caption.findViewById(R.id.back_button); + back.setOnClickListener(mOnCaptionButtonClickListener); + final View minimize = caption.findViewById(R.id.minimize_window); + minimize.setOnClickListener(mOnCaptionButtonClickListener); + final View maximize = caption.findViewById(R.id.maximize_window); + maximize.setOnClickListener(mOnCaptionButtonClickListener); + } + + void setCaptionColor(int captionColor) { + if (mResult.mRootView == null) { + return; + } + + final View caption = mResult.mRootView.findViewById(R.id.caption); + final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); + captionDrawable.setColor(captionColor); + + final int buttonTintColorRes = + Color.valueOf(captionColor).luminance() < 0.5 + ? R.color.decor_button_light_color + : R.color.decor_button_dark_color; + final ColorStateList buttonTintColor = + caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); + + final View back = caption.findViewById(R.id.back_button); + final VectorDrawable backBackground = (VectorDrawable) back.getBackground(); + backBackground.setTintList(buttonTintColor); + + final View minimize = caption.findViewById(R.id.minimize_window); + final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground(); + minimizeBackground.setTintList(buttonTintColor); + + final View maximize = caption.findViewById(R.id.maximize_window); + final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground(); + maximizeBackground.setTintList(buttonTintColor); + + final View close = caption.findViewById(R.id.close_window); + final VectorDrawable closeBackground = (VectorDrawable) close.getBackground(); + closeBackground.setTintList(buttonTintColor); + } + + private void closeDragResizeListener() { + if (mDragResizeListener == null) { + return; + } + mDragResizeListener.close(); + mDragResizeListener = null; + } + + @Override + public void close() { + closeDragResizeListener(); + super.close(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 00aab6733369..2863adcc8f5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -27,17 +27,12 @@ import android.content.Context; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; -import android.os.SystemClock; -import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; import android.view.InputChannel; -import android.view.InputDevice; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -55,7 +50,6 @@ import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; -import com.android.wm.shell.transition.Transitions; import java.util.Optional; @@ -74,9 +68,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; - private FreeformTaskTransitionStarter mTransitionStarter; - private Optional<DesktopModeController> mDesktopModeController; - private Optional<DesktopTasksController> mDesktopTasksController; + private final Optional<DesktopModeController> mDesktopModeController; + private final Optional<DesktopTasksController> mDesktopTasksController; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -84,7 +77,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); - private InputMonitorFactory mInputMonitorFactory; + private final InputMonitorFactory mInputMonitorFactory; + private TaskOperations mTaskOperations; public DesktopModeWindowDecorViewModel( Context context, @@ -136,7 +130,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { - mTransitionStarter = transitionStarter; + mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); } @Override @@ -204,13 +198,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) return; decoration.close(); - int displayId = taskInfo.displayId; + final int displayId = taskInfo.displayId; if (mEventReceiversByDisplay.contains(displayId)) { removeTaskFromEventReceiver(displayId); } } - private class CaptionTouchEventListener implements + private class DesktopModeTouchEventListener implements View.OnClickListener, View.OnTouchListener { private final int mTaskId; @@ -220,7 +214,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private int mDragPointerId = -1; - private CaptionTouchEventListener( + private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, DragResizeCallback dragResizeCallback, DragDetector dragDetector) { @@ -232,18 +226,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void onClick(View v) { - DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (id == R.id.close_window) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.removeTask(mTaskToken); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTransitionStarter.startRemoveTransition(wct); - } else { - mSyncQueue.queue(wct); - } + mTaskOperations.closeTask(mTaskToken); } else if (id == R.id.back_button) { - injectBackKey(); + mTaskOperations.injectBackKey(); } else if (id == R.id.caption_handle) { decoration.createHandleMenu(); } else if (id == R.id.desktop_button) { @@ -258,29 +246,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - private void injectBackKey() { - sendBackEvent(KeyEvent.ACTION_DOWN); - sendBackEvent(KeyEvent.ACTION_UP); - } - - private void sendBackEvent(int action) { - final long when = SystemClock.uptimeMillis(); - final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, - 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, - 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, - InputDevice.SOURCE_KEYBOARD); - - ev.setDisplayId(mContext.getDisplay().getDisplayId()); - if (!InputManager.getInstance() - .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { - Log.e(TAG, "Inject input event fail"); - } - } - @Override public boolean onTouch(View v, MotionEvent e) { boolean isDrag = false; - int id = v.getId(); + final int id = v.getId(); if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) { return false; } @@ -291,11 +260,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (e.getAction() != MotionEvent.ACTION_DOWN) { return isDrag; } - RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); if (taskInfo.isFocused) { return isDrag; } - WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mTaskToken, true /* onTop */); mSyncQueue.queue(wct); return true; @@ -306,7 +275,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { * @return {@code true} if a drag is happening; or {@code false} if it is not */ private void handleEventForMove(MotionEvent e) { - RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); if (DesktopModeStatus.isProto2Enabled() && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { return; @@ -325,16 +294,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { break; } case MotionEvent.ACTION_MOVE: { - int dragPointerIdx = e.findPointerIndex(mDragPointerId); + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragResizeCallback.onDragResizeMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - int dragPointerIdx = e.findPointerIndex(mDragPointerId); - int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId) - .stableInsets().top; + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); + final int statusBarHeight = mDisplayController + .getDisplayLayout(taskInfo.displayId).stableInsets().top; mDragResizeCallback.onDragResizeEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); if (e.getRawY(dragPointerIdx) <= statusBarHeight) { @@ -408,7 +377,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ private void incrementEventReceiverTasks(int displayId) { if (mEventReceiversByDisplay.contains(displayId)) { - EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); + final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); eventReceiver.incrementTaskNumber(); } else { createInputChannel(displayId); @@ -418,7 +387,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // If all tasks on this display are gone, we don't need to monitor its input. private void removeTaskFromEventReceiver(int displayId) { if (!mEventReceiversByDisplay.contains(displayId)) return; - EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); + final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); if (eventReceiver == null) return; eventReceiver.decrementTaskNumber(); if (eventReceiver.getTasksOnDisplay() == 0) { @@ -433,7 +402,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) { if (DesktopModeStatus.isProto2Enabled()) { - DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); + final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null || focusedDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { handleCaptionThroughStatusBar(ev); @@ -458,9 +427,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // If an UP/CANCEL action is received outside of caption bounds, turn off handle menu private void handleEventOutsideFocusedCaption(MotionEvent ev) { - int action = ev.getActionMasked(); + final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); + final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null) { return; } @@ -480,7 +449,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { // Begin drag through status bar if applicable. - DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); + final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor != null) { boolean dragFromStatusBarAllowed = false; if (DesktopModeStatus.isProto2Enabled()) { @@ -499,14 +468,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { break; } case MotionEvent.ACTION_UP: { - DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); + final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null) { mTransitionDragActive = false; return; } if (mTransitionDragActive) { mTransitionDragActive = false; - int statusBarHeight = mDisplayController + final int statusBarHeight = mDisplayController .getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top; if (ev.getY() > statusBarHeight) { if (DesktopModeStatus.isProto2Enabled()) { @@ -530,10 +499,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Nullable private DesktopModeWindowDecoration getFocusedDecor() { - int size = mWindowDecorByTaskId.size(); + final int size = mWindowDecorByTaskId.size(); DesktopModeWindowDecoration focusedDecor = null; for (int i = 0; i < size; i++) { - DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); if (decor != null && decor.isFocused()) { focusedDecor = decor; break; @@ -543,16 +512,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } private void createInputChannel(int displayId) { - InputManager inputManager = InputManager.getInstance(); - InputMonitor inputMonitor = + final InputManager inputManager = InputManager.getInstance(); + final InputMonitor inputMonitor = mInputMonitorFactory.create(inputManager, mContext); - EventReceiver eventReceiver = new EventReceiver(inputMonitor, + final EventReceiver eventReceiver = new EventReceiver(inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper()); mEventReceiversByDisplay.put(displayId, eventReceiver); } private void disposeInputChannel(int displayId) { - EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId); + final EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId); if (eventReceiver != null) { eventReceiver.dispose(); } @@ -571,7 +540,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + final DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (oldDecoration != null) { // close the old decoration if it exists to avoid two window decorations being added oldDecoration.close(); @@ -588,10 +557,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - TaskPositioner taskPositioner = + final TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener); - CaptionTouchEventListener touchEventListener = - new CaptionTouchEventListener( + final DesktopModeTouchEventListener touchEventListener = + new DesktopModeTouchEventListener( taskInfo, taskPositioner, windowDecoration.getDragDetector()); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragResizeCallback(taskPositioner); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 9c2beb9c4b2b..1a38d24a4ab1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -56,17 +56,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private View.OnClickListener mOnCaptionButtonClickListener; private View.OnTouchListener mOnCaptionTouchListener; private DragResizeCallback mDragResizeCallback; - private DragResizeInputListener mDragResizeListener; + private final DragDetector mDragDetector; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); private boolean mDesktopActive; - - private DragDetector mDragDetector; - private AdditionalWindow mHandleMenu; DesktopModeWindowDecoration( @@ -121,14 +118,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; - WindowDecorLinearLayout oldRootView = mResult.mRootView; + final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - int outsetLeftId = R.dimen.freeform_resize_handle; - int outsetTopId = R.dimen.freeform_resize_handle; - int outsetRightId = R.dimen.freeform_resize_handle; - int outsetBottomId = R.dimen.freeform_resize_handle; + final int outsetLeftId = R.dimen.freeform_resize_handle; + final int outsetTopId = R.dimen.freeform_resize_handle; + final int outsetRightId = R.dimen.freeform_resize_handle; + final int outsetBottomId = R.dimen.freeform_resize_handle; mRelayoutParams.reset(); mRelayoutParams.mRunningTaskInfo = taskInfo; @@ -152,7 +149,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mRelayoutParams.setCaptionPosition(captionLeft, captionTop); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); - taskInfo = null; // Clear it just in case we use it accidentally + // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo mTaskOrganizer.applyTransaction(wct); @@ -197,12 +194,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDragResizeCallback); } - int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); + final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) + .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - int resize_handle = mResult.mRootView.getResources() + final int resize_handle = mResult.mRootView.getResources() .getDimensionPixelSize(R.dimen.freeform_resize_handle); - int resize_corner = mResult.mRootView.getResources() + final int resize_corner = mResult.mRootView.getResources() .getDimensionPixelSize(R.dimen.freeform_resize_corner); mDragResizeListener.setGeometry( mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); @@ -212,27 +210,27 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Sets up listeners when a new root view is created. */ private void setupRootView() { - View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); caption.setOnTouchListener(mOnCaptionTouchListener); - View close = caption.findViewById(R.id.close_window); + final View close = caption.findViewById(R.id.close_window); close.setOnClickListener(mOnCaptionButtonClickListener); - View back = caption.findViewById(R.id.back_button); + final View back = caption.findViewById(R.id.back_button); back.setOnClickListener(mOnCaptionButtonClickListener); - View handle = caption.findViewById(R.id.caption_handle); + final View handle = caption.findViewById(R.id.caption_handle); handle.setOnTouchListener(mOnCaptionTouchListener); handle.setOnClickListener(mOnCaptionButtonClickListener); updateButtonVisibility(); } private void setupHandleMenu() { - View menu = mHandleMenu.mWindowViewHost.getView(); - View fullscreen = menu.findViewById(R.id.fullscreen_button); + final View menu = mHandleMenu.mWindowViewHost.getView(); + final View fullscreen = menu.findViewById(R.id.fullscreen_button); fullscreen.setOnClickListener(mOnCaptionButtonClickListener); - View desktop = menu.findViewById(R.id.desktop_button); + final View desktop = menu.findViewById(R.id.desktop_button); desktop.setOnClickListener(mOnCaptionButtonClickListener); - View split = menu.findViewById(R.id.split_screen_button); + final View split = menu.findViewById(R.id.split_screen_button); split.setOnClickListener(mOnCaptionButtonClickListener); - View more = menu.findViewById(R.id.more_button); + final View more = menu.findViewById(R.id.more_button); more.setOnClickListener(mOnCaptionButtonClickListener); } @@ -242,8 +240,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param visible whether or not the caption should be visible */ private void setCaptionVisibility(boolean visible) { - int v = visible ? View.VISIBLE : View.GONE; - View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final int v = visible ? View.VISIBLE : View.GONE; + final View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption); captionView.setVisibility(v); if (!visible) closeHandleMenu(); } @@ -264,19 +262,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Show or hide buttons */ void setButtonVisibility(boolean visible) { - int visibility = visible ? View.VISIBLE : View.GONE; - View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); - View back = caption.findViewById(R.id.back_button); - View close = caption.findViewById(R.id.close_window); + final int visibility = visible ? View.VISIBLE : View.GONE; + final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final View back = caption.findViewById(R.id.back_button); + final View close = caption.findViewById(R.id.close_window); back.setVisibility(visibility); close.setVisibility(visibility); - int buttonTintColorRes = + final int buttonTintColorRes = mDesktopActive ? R.color.decor_button_dark_color : R.color.decor_button_light_color; - ColorStateList buttonTintColor = + final ColorStateList buttonTintColor = caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); - View handle = caption.findViewById(R.id.caption_handle); - VectorDrawable handleBackground = (VectorDrawable) handle.getBackground(); + final View handle = caption.findViewById(R.id.caption_handle); + final VectorDrawable handleBackground = (VectorDrawable) handle.getBackground(); handleBackground.setTintList(buttonTintColor); caption.getBackground().setTint(visible ? Color.WHITE : Color.TRANSPARENT); } @@ -297,12 +295,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display handle menu window */ void createHandleMenu() { - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); final Resources resources = mDecorWindowContext.getResources(); - int x = mRelayoutParams.mCaptionX; - int y = mRelayoutParams.mCaptionY; - int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); - int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + final int x = mRelayoutParams.mCaptionX; + final int y = mRelayoutParams.mCaptionY; + final int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + final int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); String namePrefix = "Caption Menu"; mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY, @@ -353,8 +351,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @return the point of the input in local space */ private PointF offsetCaptionLocation(MotionEvent ev) { - PointF result = new PointF(ev.getX(), ev.getY()); - Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) + final PointF result = new PointF(ev.getX(), ev.getY()); + final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) .positionInParent; result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); result.offset(-positionInParent.x, -positionInParent.y); @@ -370,8 +368,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) { if (mResult.mRootView == null) return false; - PointF inputPoint = offsetCaptionLocation(ev); - View view = mResult.mRootView.findViewById(layoutId); + final PointF inputPoint = offsetCaptionLocation(ev); + final View view = mResult.mRootView.findViewById(layoutId); return view != null && view.pointInView(inputPoint.x, inputPoint.y, 0); } @@ -389,20 +387,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void checkClickEvent(MotionEvent ev) { if (mResult.mRootView == null) return; - View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); - PointF inputPoint = offsetCaptionLocation(ev); + final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final PointF inputPoint = offsetCaptionLocation(ev); if (!isHandleMenuActive()) { - View handle = caption.findViewById(R.id.caption_handle); + final View handle = caption.findViewById(R.id.caption_handle); clickIfPointInView(inputPoint, handle); } else { - View menu = mHandleMenu.mWindowViewHost.getView(); - View fullscreen = menu.findViewById(R.id.fullscreen_button); + final View menu = mHandleMenu.mWindowViewHost.getView(); + final View fullscreen = menu.findViewById(R.id.fullscreen_button); if (clickIfPointInView(inputPoint, fullscreen)) return; - View desktop = menu.findViewById(R.id.desktop_button); + final View desktop = menu.findViewById(R.id.desktop_button); if (clickIfPointInView(inputPoint, desktop)) return; - View split = menu.findViewById(R.id.split_screen_button); + final View split = menu.findViewById(R.id.split_screen_button); if (clickIfPointInView(inputPoint, split)) return; - View more = menu.findViewById(R.id.more_button); + final View more = menu.findViewById(R.id.more_button); clickIfPointInView(inputPoint, more); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java new file mode 100644 index 000000000000..aea340464304 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.SystemClock; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.transition.Transitions; + +/** + * Utility class to handle task operations performed on a window decoration. + */ +class TaskOperations { + private static final String TAG = "TaskOperations"; + + private final FreeformTaskTransitionStarter mTransitionStarter; + private final Context mContext; + private final SyncTransactionQueue mSyncQueue; + + TaskOperations(FreeformTaskTransitionStarter transitionStarter, Context context, + SyncTransactionQueue syncQueue) { + mTransitionStarter = transitionStarter; + mContext = context; + mSyncQueue = syncQueue; + } + + void injectBackKey() { + sendBackEvent(KeyEvent.ACTION_DOWN); + sendBackEvent(KeyEvent.ACTION_UP); + } + + private void sendBackEvent(int action) { + final long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, + 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, + 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + + ev.setDisplayId(mContext.getDisplay().getDisplayId()); + if (!InputManager.getInstance() + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { + Log.e(TAG, "Inject input event fail"); + } + } + + void closeTask(WindowContainerToken taskToken) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.removeTask(taskToken); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startRemoveTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } + + void minimizeTask(WindowContainerToken taskToken) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(taskToken, false); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startMinimizedModeTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } + + void maximizeTask(RunningTaskInfo taskInfo) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN + ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM; + int displayWindowingMode = + taskInfo.configuration.windowConfiguration.getDisplayWindowingMode(); + wct.setWindowingMode(taskInfo.token, + targetWindowingMode == displayWindowingMode + ? WINDOWING_MODE_UNDEFINED : targetWindowingMode); + if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { + wct.setBounds(taskInfo.token, null); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct); + } else { + mSyncQueue.queue(wct); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index a49a300995e6..20631f85453f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -47,6 +47,10 @@ class TaskPositioner implements DragResizeCallback { private int mCtrlType; private DragStartListener mDragStartListener; + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) { + this(taskOrganizer, windowDecoration, dragStartListener -> {}); + } + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, DragStartListener dragStartListener) { mTaskOrganizer = taskOrganizer; 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 2e328b0736dd..2754496a6f3f 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 @@ -53,6 +53,7 @@ import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.BackEvent; +import android.window.BackMotionEvent; import android.window.BackNavigationInfo; import android.window.IBackNaviAnimationController; import android.window.IOnBackInvokedCallback; @@ -246,10 +247,11 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); + ArgumentCaptor<BackMotionEvent> backEventCaptor = + ArgumentCaptor.forClass(BackMotionEvent.class); verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); - verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class)); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackMotionEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back @@ -271,17 +273,18 @@ public class BackAnimationControllerTest extends ShellTestCase { RemoteAnimationTarget animationTarget = createAnimationTarget(); IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); - ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); + ArgumentCaptor<BackMotionEvent> backEventCaptor = + ArgumentCaptor.forClass(BackMotionEvent.class); createNavigationInfo(animationTarget, null, null, BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false); triggerBackGesture(); - verify(appCallback, never()).onBackStarted(any(BackEvent.class)); + verify(appCallback, never()).onBackStarted(any(BackMotionEvent.class)); verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(appCallback, times(1)).onBackInvoked(); - verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class)); + verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackMotionEvent.class)); verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mIOnBackInvokedCallback, never()).onBackInvoked(); } @@ -314,7 +317,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class)); } @Test @@ -333,7 +336,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class)); } @@ -349,7 +352,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back 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 index 3aefc3f03a8a..ba9c159bad28 100644 --- 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.back; import static org.junit.Assert.assertEquals; import android.window.BackEvent; +import android.window.BackMotionEvent; import org.junit.Before; import org.junit.Test; @@ -38,7 +39,7 @@ public class TouchTrackerTest { @Test public void generatesProgress_onStart() { mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); - BackEvent event = mTouchTracker.createStartEvent(null); + BackMotionEvent event = mTouchTracker.createStartEvent(null); assertEquals(event.getProgress(), 0f, 0f); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index e6711aca19c1..8b025cd7c246 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -32,6 +34,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.content.Intent; import android.content.LocusId; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -94,6 +97,7 @@ public class BubbleDataTest extends ShellTestCase { private Bubble mBubbleInterruptive; private Bubble mBubbleDismissed; private Bubble mBubbleLocusId; + private Bubble mAppBubble; private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; @@ -178,6 +182,11 @@ public class BubbleDataTest extends ShellTestCase { mBubbleMetadataFlagListener, mPendingIntentCanceledListener, mMainExecutor); + + Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + appBubbleIntent.setPackage(mContext.getPackageName()); + mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor); + mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, @@ -1089,6 +1098,18 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + @Test + public void test_removeAppBubble_skipsOverflow() { + mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, + false /* showInShade */); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble); + + mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE); + + assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 08af3d3eecfe..7997a7ed7f7f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -279,7 +279,7 @@ public class DesktopModeControllerTest extends ShellTestCase { } @Test - public void testShowDesktopApps_appsAlreadyVisible_doesNothing() { + public void testShowDesktopApps_appsAlreadyVisible_bringsToFront() { final RunningTaskInfo task1 = createFreeformTask(); mDesktopModeTaskRepository.addActiveTask(task1.taskId); mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); @@ -294,8 +294,17 @@ public class DesktopModeControllerTest extends ShellTestCase { mController.showDesktopApps(); final WindowContainerTransaction wct = getBringAppsToFrontTransaction(); - // No reordering needed. - assertThat(wct.getHierarchyOps()).isEmpty(); + // Check wct has reorder calls + assertThat(wct.getHierarchyOps()).hasSize(2); + // Task 1 appeared first, must be first reorder to top. + HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder()); + + // Task 2 appeared last, must be last reorder to top. + HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder()); } @Test @@ -325,6 +334,41 @@ public class DesktopModeControllerTest extends ShellTestCase { } @Test + public void testGetVisibleTaskCount_noTasks_returnsZero() { + assertThat(mController.getVisibleTaskCount()).isEqualTo(0); + } + + @Test + public void testGetVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */); + + RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */); + + assertThat(mController.getVisibleTaskCount()).isEqualTo(2); + } + + @Test + public void testGetVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */); + + RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, false /* visible */); + + assertThat(mController.getVisibleTaskCount()).isEqualTo(1); + } + + @Test public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() { when(DesktopModeStatus.isActive(any())).thenReturn(false); WindowContainerTransaction wct = mController.handleRequest( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 1e43a5983821..45cb3a062cc5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -141,6 +141,36 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun getVisibleTaskCount() { + // No tasks, count is 0 + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + + // New task increments count to 1 + repo.updateVisibleFreeformTasks(taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Visibility update to same task does not increase count + repo.updateVisibleFreeformTasks(taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Second task visible increments count + repo.updateVisibleFreeformTasks(taskId = 2, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(2) + + // Hiding a task decrements count + repo.updateVisibleFreeformTasks(taskId = 1, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Hiding all tasks leaves count at 0 + repo.updateVisibleFreeformTasks(taskId = 2, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + + // Hiding a not existing task, count remains at 0 + repo.updateVisibleFreeformTasks(taskId = 999, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + } + + @Test fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() { repo.addOrMoveFreeformTaskToTop(5) repo.addOrMoveFreeformTaskToTop(6) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 9a92879bde1f..f16beeed57a3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -150,8 +150,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_appsAlreadyVisible_doesNothing() { - setUpHomeTask() + fun showDesktopApps_appsAlreadyVisible_bringsToFront() { + val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -159,7 +159,12 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.showDesktopApps() - verifyWCTNotExecuted() + val wct = getLatestWct() + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) } @Test @@ -192,6 +197,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun getVisibleTaskCount_noTasks_returnsZero() { + assertThat(controller.getVisibleTaskCount()).isEqualTo(0) + } + + @Test + fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount()).isEqualTo(2) + } + + @Test + fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskHidden) + assertThat(controller.getVisibleTaskCount()).isEqualTo(1) + } + + @Test fun moveToDesktop() { val task = setUpFullscreenTask() controller.moveToDesktop(task) @@ -207,6 +233,23 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToDesktop_otherFreeformTasksBroughtToFront() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask) + + with(getLatestWct()) { + assertThat(hierarchyOps).hasSize(3) + assertReorderSequence(homeTask, freeformTask, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test fun moveToFullscreen() { val task = setUpFreeformTask() controller.moveToFullscreen(task) @@ -406,3 +449,9 @@ private fun WindowContainerTransaction.assertReorderAt(index: Int, task: Running assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) } + +private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { + for (i in tasks.indices) { + assertReorderAt(i, tasks[i]) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index 262e4290ef44..298d0a624869 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -64,7 +64,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { initializeMockResources(); mPipBoundsState = new PipBoundsState(mContext); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {}); + new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}); mPipBoundsState.setDisplayLayout( new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 90880772b25d..17e7d74c57e0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -98,7 +98,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { mPipBoundsState = new PipBoundsState(mContext); mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {}); + new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}); mMainExecutor = new TestShellExecutor(); mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index 3bd2ae76ebfd..c1993b25030b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -37,7 +37,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; -import com.android.wm.shell.pip.PipKeepClearAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; @@ -90,8 +90,8 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mPipBoundsState = new PipBoundsState(mContext); final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm(); - final PipKeepClearAlgorithm pipKeepClearAlgorithm = - new PipKeepClearAlgorithm() {}; + final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm = + new PipKeepClearAlgorithmInterface() {}; final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm); final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 474d6aaf4623..8ad2932b69e4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -34,7 +34,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; -import com.android.wm.shell.pip.PipKeepClearAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; @@ -106,7 +106,7 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipBoundsState = new PipBoundsState(mContext); mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, - new PipKeepClearAlgorithm() {}); + new PipKeepClearAlgorithmInterface() {}); PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator); diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index 468a97630e19..15920940140c 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -59,6 +59,18 @@ public class IllustrationPreference extends Preference { private Uri mImageUri; private Drawable mImageDrawable; private View mMiddleGroundView; + private OnBindListener mOnBindListener; + + /** + * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + */ + public interface OnBindListener { + /** + * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + * @param animationView the animation view for this preference. + */ + void onBind(LottieAnimationView animationView); + } private final Animatable2.AnimationCallback mAnimationCallback = new Animatable2.AnimationCallback() { @@ -133,6 +145,17 @@ public class IllustrationPreference extends Preference { if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) { ColorUtils.applyDynamicColors(getContext(), illustrationView); } + + if (mOnBindListener != null) { + mOnBindListener.onBind(illustrationView); + } + } + + /** + * Sets a listener to be notified when the views are binded. + */ + public void setOnBindListener(OnBindListener listener) { + mOnBindListener = listener; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 7ec0fcdfeb64..d2e615a77471 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -190,6 +190,10 @@ public class InfoMediaManager extends MediaManager { return !isGroup; } + boolean preferRouteListingOrdering() { + return false; + } + /** * Remove a {@code device} from current media. * diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index f4355c39819f..7458f0106283 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -213,6 +213,15 @@ public class LocalMediaManager implements BluetoothCallback { } /** + * Returns if media app establishes a preferred route listing order. + * + * @return True if route list ordering exist and not using system ordering, false otherwise. + */ + public boolean isPreferenceRouteListingExist() { + return mInfoMediaManager.preferRouteListingOrdering(); + } + + /** * Start scan connected MediaDevice */ public void startScan() { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index c829bc316246..d6586db1b50a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -184,6 +184,15 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { */ public abstract String getId(); + /** + * Checks if device is suggested device from application + * + * @return true if device is suggested device + */ + public boolean isSuggestedDevice() { + return false; + } + void setConnectedRecord() { mConnectedRecord++; ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java index 29549d9a7fa7..103512d4a28a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java @@ -61,6 +61,8 @@ public class IllustrationPreferenceTest { private PreferenceViewHolder mViewHolder; private FrameLayout mMiddleGroundLayout; private final Context mContext = ApplicationProvider.getApplicationContext(); + private IllustrationPreference.OnBindListener mOnBindListener; + private LottieAnimationView mOnBindListenerAnimationView; @Before public void setUp() { @@ -82,6 +84,12 @@ public class IllustrationPreferenceTest { final AttributeSet attributeSet = Robolectric.buildAttributeSet().build(); mPreference = new IllustrationPreference(mContext, attributeSet); + mOnBindListener = new IllustrationPreference.OnBindListener() { + @Override + public void onBind(LottieAnimationView animationView) { + mOnBindListenerAnimationView = animationView; + } + }; } @Test @@ -186,4 +194,25 @@ public class IllustrationPreferenceTest { assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight); assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight); } + + @Test + public void setOnBindListener_isNotified() { + mOnBindListenerAnimationView = null; + mPreference.setOnBindListener(mOnBindListener); + + mPreference.onBindViewHolder(mViewHolder); + + assertThat(mOnBindListenerAnimationView).isNotNull(); + assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView); + } + + @Test + public void setOnBindListener_notNotified() { + mOnBindListenerAnimationView = null; + mPreference.setOnBindListener(null); + + mPreference.onBindViewHolder(mViewHolder); + + assertThat(mOnBindListenerAnimationView).isNull(); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 1b0b6b4ff796..211030a90c47 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -123,7 +123,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, Settings.Secure.ACTIVE_UNLOCK_ON_WAKE, Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT, Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 4fa490ffaed5..0539f09e20d3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -178,7 +178,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR); - VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SFPS_PERFORMANT_AUTH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index cd0fbea6a2d2..c67cdfcfa164 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -271,6 +271,7 @@ android_library { "LowLightDreamLib", "motion_tool_lib", "androidx.core_core-animation-testing-nodeps", + "androidx.compose.ui_ui", ], } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 810dd33ae17a..c685d62cbf8e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -292,7 +292,7 @@ <queries> <intent> - <action android:name="android.intent.action.NOTES" /> + <action android:name="android.intent.action.CREATE_NOTE" /> </intent> </queries> @@ -411,7 +411,6 @@ <service android:name=".screenshot.ScreenshotCrossProfileService" android:permission="com.android.systemui.permission.SELF" - android:process=":screenshot_cross_profile" android:exported="false" /> <service android:name=".screenrecord.RecordingService" /> @@ -664,6 +663,20 @@ android:excludeFromRecents="true" android:exported="true" /> + <!-- started from Telecomm(CallsManager) --> + <activity + android:name=".telephony.ui.activity.SwitchToManagedProfileForCallActivity" + android:excludeFromRecents="true" + android:exported="true" + android:finishOnCloseSystemDialogs="true" + android:permission="android.permission.MODIFY_PHONE_STATE" + android:theme="@style/Theme.SystemUI.Dialog.Alert"> + <intent-filter> + <action android:name="android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <!-- platform logo easter egg activity --> <activity android:name=".DessertCase" diff --git a/packages/SystemUI/README.md b/packages/SystemUI/README.md index ee8d02301d5d..2910bba71341 100644 --- a/packages/SystemUI/README.md +++ b/packages/SystemUI/README.md @@ -5,46 +5,72 @@ SystemUI is a persistent process that provides UI for the system but outside of the system_server process. -The starting point for most of sysui code is a list of services that extend -SystemUI that are started up by SystemUIApplication. These services then depend -on some custom dependency injection provided by Dependency. - Inputs directed at sysui (as opposed to general listeners) generally come in through IStatusBar. Outputs from sysui are through a variety of private APIs to the android platform all over. ## SystemUIApplication -When SystemUIApplication starts up, it will start up the services listed in -config_systemUIServiceComponents or config_systemUIServiceComponentsPerUser. +When SystemUIApplication starts up, it instantiates a Dagger graph from which +various pieces of the application are built. -Each of these services extend SystemUI. SystemUI provides them with a Context -and gives them callbacks for onConfigurationChanged (this historically was -the main path for onConfigurationChanged, now also happens through -ConfigurationController). They also receive a callback for onBootCompleted -since these objects may be started before the device has finished booting. +To support customization, SystemUIApplication relies on the AndroidManifest.xml +having an `android.app.AppComponentFactory` specified. Specifically, it relies +on an `AppComponentFactory` that subclases `SystemUIAppComponentFactoryBase`. +Implementations of this abstract base class must override +`#createSystemUIInitializer(Context)` which returns a `SystemUIInitializer`. +`SystemUIInitializer` primary job in turn is to intialize and return the Dagger +root component back to the `SystemUIApplication`. -Each SystemUI service is expected to be a major part of system ui and the -goal is to minimize communication between them. So in general they should be -relatively silo'd. +Writing a custom `SystemUIAppComponentFactoryBase` and `SystemUIInitializer`, +should be enough for most implementations to stand up a customized Dagger +graph, and launch a custom version of SystemUI. -## Dependencies +## Dagger / Dependency Injection -The first SystemUI service that is started should always be Dependency. -Dependency provides a static method for getting a hold of dependencies that -have a lifecycle that spans sysui. Dependency has code for how to create all -dependencies manually added. SystemUIFactory is also capable of -adding/replacing these dependencies. +See [dagger.md](docs/dagger.md) and https://dagger.dev/. -Dependencies are lazily initialized, so if a Dependency is never referenced at -runtime, it will never be created. +## CoreStartable -If an instantiated dependency implements Dumpable it will be included in dumps -of sysui (and bug reports), allowing it to include current state information. -This is how \*Controllers dump state to bug reports. +The starting point for most of SystemUI code is a list of classes that +implement `CoreStartable` that are started up by SystemUIApplication. +CoreStartables are like miniature services. They have their `#start` method +called after being instantiated, and a reference to them is stored inside +SystemUIApplication. They are in charge of their own behavior beyond this, +registering and unregistering with the rest of the system as needed. + +`CoreStartable` also receives a callback for `#onBootCompleted` +since these objects may be started before the device has finished booting. -If an instantiated dependency implements ConfigurationChangeReceiver it will -receive onConfigurationChange callbacks when the configuration changes. +`CoreStartable` is an ideal place to add new features and functionality +that does not belong directly under the umbrella of an existing feature. +It is better to define a new `CoreStartable` than to stick unrelated +initialization code together in catch-all methods. + +CoreStartables are tied to application startup via Dagger: + +```kotlin +class FeatureStartable +@Inject +constructor( + /* ... */ +) : CoreStartable { + override fun start() { + // ... + } +} + +@Module +abstract class FeatureModule { + @Binds + @IntoMap + @ClassKey(FeatureStartable::class) + abstract fun bind(impl: FeatureStartable): CoreStartable +} +``` + +Including `FeatureModule` in the Dagger graph such as this will ensure that +`FeatureStartable` gets constructed and that its `#start` method is called. ## IStatusBar @@ -64,12 +90,6 @@ across sysui. Such as when StatusBar calls CommandQueue#recomputeDisableFlags. This is generally used a shortcut to directly trigger CommandQueue rather than calling StatusManager and waiting for the call to come back to IStatusBar. -## Default SystemUI services list - -### [com.android.systemui.Dependency](/packages/SystemUI/src/com/android/systemui/Dependency.java) - -Provides custom dependency injection. - ### [com.android.systemui.util.NotificationChannels](/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java) Creates/initializes the channels sysui uses when posting notifications. @@ -88,11 +108,11 @@ activity. It provides this cached data to RecentsActivity when it is started. Registers all the callbacks/listeners required to show the Volume dialog when it should be shown. -### [com.android.systemui.status.phone.StatusBar](/packages/SystemUI/src/com/android/systemui/status/phone/StatusBar.java) +### [com.android.systemui.status.phone.CentralSurfaces](/packages/SystemUI/src/com/android/systemui/status/phone/CentralSurfaces.java) This shows the UI for the status bar and the notification shade it contains. It also contains a significant amount of other UI that interacts with these -surfaces (keyguard, AOD, etc.). StatusBar also contains a notification listener +surfaces (keyguard, AOD, etc.). CentralSurfaces also contains a notification listener to receive notification callbacks. ### [com.android.systemui.usb.StorageNotification](/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index fe349f21e36e..8f70dcc02289 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -37,6 +37,8 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.animation.Interpolator import android.view.animation.PathInterpolator +import androidx.annotation.BinderThread +import androidx.annotation.UiThread import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils import kotlin.math.roundToInt @@ -226,7 +228,7 @@ class ActivityLaunchAnimator( // If we expect an animation, post a timeout to cancel it in case the remote animation is // never started. if (willAnimate) { - runner.postTimeout() + runner.delegate.postTimeout() // Hide the keyguard using the launch animation instead of the default unlock animation. if (hideKeyguardWithAnimation) { @@ -389,14 +391,51 @@ class ActivityLaunchAnimator( fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {} } - class Runner( + @VisibleForTesting + inner class Runner( + controller: Controller, + callback: Callback, + /** The animator to use to animate the window launch. */ + launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, + /** Listener for animation lifecycle events. */ + listener: Listener? = null + ) : IRemoteAnimationRunner.Stub() { + private val context = controller.launchContainer.context + internal val delegate: AnimationDelegate + + init { + delegate = AnimationDelegate(controller, callback, launchAnimator, listener) + } + + @BinderThread + override fun onAnimationStart( + transit: Int, + apps: Array<out RemoteAnimationTarget>?, + wallpapers: Array<out RemoteAnimationTarget>?, + nonApps: Array<out RemoteAnimationTarget>?, + finishedCallback: IRemoteAnimationFinishedCallback? + ) { + context.mainExecutor.execute { + delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) + } + } + + @BinderThread + override fun onAnimationCancelled(isKeyguardOccluded: Boolean) { + context.mainExecutor.execute { delegate.onAnimationCancelled(isKeyguardOccluded) } + } + } + + class AnimationDelegate + @JvmOverloads + constructor( private val controller: Controller, private val callback: Callback, /** The animator to use to animate the window launch. */ private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, /** Listener for animation lifecycle events. */ private val listener: Listener? = null - ) : IRemoteAnimationRunner.Stub() { + ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { private val launchContainer = controller.launchContainer private val context = launchContainer.context private val transactionApplierView = @@ -419,6 +458,7 @@ class ActivityLaunchAnimator( // posting it. private var onTimeout = Runnable { onAnimationTimedOut() } + @UiThread internal fun postTimeout() { launchContainer.postDelayed(onTimeout, LAUNCH_TIMEOUT) } @@ -427,19 +467,20 @@ class ActivityLaunchAnimator( launchContainer.removeCallbacks(onTimeout) } + @UiThread override fun onAnimationStart( @WindowManager.TransitionOldType transit: Int, apps: Array<out RemoteAnimationTarget>?, wallpapers: Array<out RemoteAnimationTarget>?, nonApps: Array<out RemoteAnimationTarget>?, - iCallback: IRemoteAnimationFinishedCallback? + callback: IRemoteAnimationFinishedCallback? ) { removeTimeout() // The animation was started too late and we already notified the controller that it // timed out. if (timedOut) { - iCallback?.invoke() + callback?.invoke() return } @@ -449,7 +490,7 @@ class ActivityLaunchAnimator( return } - context.mainExecutor.execute { startAnimation(apps, nonApps, iCallback) } + startAnimation(apps, nonApps, callback) } private fun startAnimation( @@ -687,6 +728,7 @@ class ActivityLaunchAnimator( controller.onLaunchAnimationCancelled() } + @UiThread override fun onAnimationCancelled(isKeyguardOccluded: Boolean) { if (timedOut) { return @@ -695,10 +737,9 @@ class ActivityLaunchAnimator( Log.i(TAG, "Remote animation was cancelled") cancelled = true removeTimeout() - context.mainExecutor.execute { - animation?.cancel() - controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded) - } + + animation?.cancel() + controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded) } private fun IRemoteAnimationFinishedCallback.invoke() { 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 54aa3516d867..a3ed0856c60a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -237,13 +237,17 @@ constructor( openedDialogs.firstOrNull { it.dialog.window.decorView.viewRootImpl == controller.viewRoot } - val animateFrom = + val controller = animatedParent?.dialogContentWithBackground?.let { Controller.fromView(it, controller.cuj) } ?: controller - if (animatedParent == null && animateFrom !is LaunchableView) { + if ( + animatedParent == null && + controller is ViewDialogLaunchAnimatorController && + controller.source !is LaunchableView + ) { // Make sure the View we launch from implements LaunchableView to avoid visibility // issues. Given that we don't own dialog decorViews so we can't enforce it for launches // from a dialog. @@ -272,7 +276,7 @@ constructor( launchAnimator, callback, interactionJankMonitor, - animateFrom, + controller, onDialogDismissed = { openedDialogs.remove(it) }, dialog = dialog, animateBackgroundBoundsChange, @@ -366,7 +370,7 @@ constructor( val dialog = animatedDialog.dialog // Don't animate if the dialog is not showing or if we are locked and going to show the - // bouncer. + // primary bouncer. if ( !dialog.isShowing || (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) @@ -791,13 +795,13 @@ private class AnimatedDialog( // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a // one-off synchronization to make sure that this is done in sync between the two different // windows. + controller.startDrawingInOverlayOf(decorView) synchronizeNextDraw( then = { isSourceDrawnInDialog = true maybeStartLaunchAnimation() } ) - controller.startDrawingInOverlayOf(decorView) } /** diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index 0028d13ffd5e..dfac02d99c4d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -195,14 +195,16 @@ open class GhostedViewLaunchAnimatorController @JvmOverloads constructor( backgroundDrawable = WrappedDrawable(background) backgroundView?.background = backgroundDrawable + // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be + // called before `GhostView.addGhost()` is called because the latter will change the + // *transition* visibility, which won't be blocked and will affect the normal View + // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration. + (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + // Create a ghost of the view that will be moving and fading out. This allows to fade out // the content before fading out the background. ghostView = GhostView.addGhost(ghostedView, launchContainer) - // The ghost was just created, so ghostedView is currently invisible. We need to make sure - // that it stays invisible as long as we are animating. - (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) - val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX matrix.getValues(initialGhostViewMatrixValues) @@ -297,14 +299,19 @@ open class GhostedViewLaunchAnimatorController @JvmOverloads constructor( backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha GhostView.removeGhost(ghostedView) - (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(false) launchContainerOverlay.remove(backgroundView) - // Make sure that the view is considered VISIBLE by accessibility by first making it - // INVISIBLE then VISIBLE (see b/204944038#comment17 for more info). - ghostedView.visibility = View.INVISIBLE - ghostedView.visibility = View.VISIBLE - ghostedView.invalidate() + if (ghostedView is LaunchableView) { + // Restore the ghosted view visibility. + ghostedView.setShouldBlockVisibilityChanges(false) + } else { + // Make the ghosted view visible. We ensure that the view is considered VISIBLE by + // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 + // for more info). + ghostedView.visibility = View.INVISIBLE + ghostedView.visibility = View.VISIBLE + ghostedView.invalidate() + } } companion object { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt index 67b59e0e9928..774255be4007 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt @@ -21,15 +21,19 @@ import android.view.View /** A view that can expand/launch into an app or a dialog. */ interface LaunchableView { /** - * Set whether this view should block/postpone all visibility changes. This ensures that this - * view: + * Set whether this view should block/postpone all calls to [View.setVisibility]. This ensures + * that this view: * - remains invisible during the launch animation given that it is ghosted and already drawn * somewhere else. * - remains invisible as long as a dialog expanded from it is shown. * - restores its expected visibility once the dialog expanded from it is dismissed. * - * Note that when this is set to true, both the [normal][android.view.View.setVisibility] and - * [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked. + * When `setShouldBlockVisibilityChanges(false)` is called, then visibility of the View should + * be restored to its expected value, i.e. it should have the visibility of the last call to + * `View.setVisibility()` that was made after `setShouldBlockVisibilityChanges(true)`, if any, + * or the original view visibility otherwise. + * + * Note that calls to [View.setTransitionVisibility] shouldn't be blocked. * * @param block whether we should block/postpone all calls to `setVisibility` and * `setTransitionVisibility`. @@ -46,27 +50,31 @@ class LaunchableViewDelegate( * super.setVisibility(visibility). */ private val superSetVisibility: (Int) -> Unit, - - /** - * The lambda that should set the actual transition visibility of [view], usually by calling - * super.setTransitionVisibility(visibility). - */ - private val superSetTransitionVisibility: (Int) -> Unit, -) { +) : LaunchableView { private var blockVisibilityChanges = false private var lastVisibility = view.visibility /** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */ - fun setShouldBlockVisibilityChanges(block: Boolean) { + override fun setShouldBlockVisibilityChanges(block: Boolean) { if (block == blockVisibilityChanges) { return } blockVisibilityChanges = block if (block) { + // Save the current visibility for later. lastVisibility = view.visibility } else { - superSetVisibility(lastVisibility) + // Restore the visibility. To avoid accessibility issues, we change the visibility twice + // which makes sure that we trigger a visibility flag change (see b/204944038#comment17 + // for more info). + if (lastVisibility == View.VISIBLE) { + superSetVisibility(View.INVISIBLE) + superSetVisibility(View.VISIBLE) + } else { + superSetVisibility(View.VISIBLE) + superSetVisibility(lastVisibility) + } } } @@ -79,16 +87,4 @@ class LaunchableViewDelegate( superSetVisibility(visibility) } - - /** Call this when [View.setTransitionVisibility] is called. */ - fun setTransitionVisibility(visibility: Int) { - if (blockVisibilityChanges) { - // View.setTransitionVisibility just sets the visibility flag, so we don't have to save - // the transition visibility separately from the normal visibility. - lastVisibility = visibility - return - } - - superSetTransitionVisibility(visibility) - } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt new file mode 100644 index 000000000000..337408bb9c5d --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt @@ -0,0 +1,30 @@ +package com.android.systemui.animation + +import android.annotation.UiThread +import android.view.IRemoteAnimationFinishedCallback +import android.view.RemoteAnimationTarget +import android.view.WindowManager + +/** + * A component capable of running remote animations. + * + * Expands the IRemoteAnimationRunner API by allowing for different types of more specialized + * callbacks. + */ +interface RemoteAnimationDelegate<in T : IRemoteAnimationFinishedCallback> { + /** + * Called on the UI thread when the animation targets are received. Sets up and kicks off the + * animation. + */ + @UiThread + fun onAnimationStart( + @WindowManager.TransitionOldType transit: Int, + apps: Array<out RemoteAnimationTarget>?, + wallpapers: Array<out RemoteAnimationTarget>?, + nonApps: Array<out RemoteAnimationTarget>?, + callback: T? + ) + + /** Called on the UI thread when a signal is received to cancel the animation. */ + @UiThread fun onAnimationCancelled(isKeyguardOccluded: Boolean) +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt index 964ef8c88098..9257f99efe96 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -25,7 +25,7 @@ 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, + internal val source: View, override val cuj: DialogCuj?, ) : DialogLaunchAnimator.Controller { override val viewRoot: ViewRootImpl? @@ -34,23 +34,29 @@ internal constructor( override val sourceIdentity: Any = source override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Delay the calls to `source.setVisibility()` during the animation. This must be called + // before `GhostView.addGhost()` is called because the latter will change the *transition* + // visibility, which won't be blocked and will affect the normal View visibility that is + // saved by `setShouldBlockVisibilityChanges()` for a later restoration. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + // 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 + // already done by the launch controller created below. + + if (source is LaunchableView) { + // Make sure we allow the source to change its visibility again and restore its previous + // value. + source.setShouldBlockVisibilityChanges(false) + } else { + // We made the source invisible earlier, so let's make it visible again. + source.visibility = View.VISIBLE + } } override fun createLaunchController(): LaunchAnimator.Controller { @@ -67,10 +73,14 @@ internal constructor( 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 + // At this point the view visibility is restored by the delegate, so we delay the + // visibility changes again and make it invisible while the dialog is shown. + if (source is LaunchableView) { + source.setShouldBlockVisibilityChanges(true) + source.setTransitionVisibility(View.INVISIBLE) + } else { + source.visibility = View.INVISIBLE + } } } } @@ -90,13 +100,15 @@ internal constructor( } 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 + if (source is LaunchableView) { + // Make sure we allow the source to change its visibility again. + source.setShouldBlockVisibilityChanges(false) + } else { + // 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 + } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt index 67159512a701..79bc2f432ded 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -57,7 +57,7 @@ data class TurbulenceNoiseAnimationConfig( val onAnimationEnd: Runnable? = null ) { companion object { - const val DEFAULT_MAX_DURATION_IN_MILLIS = 7500f + const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec const val DEFAULT_EASING_DURATION_IN_MILLIS = 750f const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f const val DEFAULT_NOISE_GRID_COUNT = 1.2f diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt new file mode 100644 index 000000000000..1f6e60359eb8 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UFile +import org.jetbrains.uast.UImportStatement + +/** + * Detects violations of the Dependency Rule of Clean Architecture. + * + * The rule states that code in each layer may only depend on code in the same layer or the layer + * directly "beneath" that layer in the layer diagram. + * + * In System UI, we have three layers; from top to bottom, they are: ui, domain, and data. As a + * convention, was used packages with those names to place code in the appropriate layer. We also + * make an exception and allow for shared models to live under a separate package named "shared" to + * avoid code duplication. + * + * For more information, please see go/sysui-arch. + */ +@Suppress("UnstableApiUsage") +class CleanArchitectureDependencyViolationDetector : Detector(), Detector.UastScanner { + override fun getApplicableUastTypes(): List<Class<out UElement>> { + return listOf(UFile::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitFile(node: UFile) { + // Check which Clean Architecture layer this file belongs to: + matchingLayer(node.packageName)?.let { layer -> + // The file matches with a Clean Architecture layer. Let's check all of its + // imports. + node.imports.forEach { importStatement -> + visitImportStatement(context, layer, importStatement) + } + } + } + } + } + + private fun visitImportStatement( + context: JavaContext, + layer: Layer, + importStatement: UImportStatement, + ) { + val importText = importStatement.importReference?.asSourceString() ?: return + val importedLayer = matchingLayer(importText) ?: return + + // Now check whether the layer of the file may depend on the layer of the import. + if (!layer.mayDependOn(importedLayer)) { + context.report( + issue = ISSUE, + scope = importStatement, + location = context.getLocation(importStatement), + message = + "The ${layer.packageNamePart} layer may not depend on" + + " the ${importedLayer.packageNamePart} layer.", + ) + } + } + + private fun matchingLayer(packageName: String): Layer? { + val packageNameParts = packageName.split(".").toSet() + return Layer.values() + .filter { layer -> packageNameParts.contains(layer.packageNamePart) } + .takeIf { it.size == 1 } + ?.first() + } + + private enum class Layer( + val packageNamePart: String, + val canDependOn: Set<Layer>, + ) { + SHARED( + packageNamePart = "shared", + canDependOn = emptySet(), // The shared layer may not depend on any other layer. + ), + DATA( + packageNamePart = "data", + canDependOn = setOf(SHARED), + ), + DOMAIN( + packageNamePart = "domain", + canDependOn = setOf(SHARED, DATA), + ), + UI( + packageNamePart = "ui", + canDependOn = setOf(DOMAIN, SHARED), + ), + ; + + fun mayDependOn(otherLayer: Layer): Boolean { + return this == otherLayer || canDependOn.contains(otherLayer) + } + } + + companion object { + @JvmStatic + val ISSUE = + Issue.create( + id = "CleanArchitectureDependencyViolation", + briefDescription = "Violation of the Clean Architecture Dependency Rule.", + explanation = + """ + Following the \"Dependency Rule\" from Clean Architecture, every layer of code \ + can only depend code in its own layer or code in the layer directly \ + \"beneath\" it. Therefore, the UI layer can only depend on the" Domain layer \ + and the Domain layer can only depend on the Data layer. We" do make an \ + exception to allow shared models to exist and be shared across layers by \ + placing them under shared/model, which should be done with care. For more \ + information about Clean Architecture in System UI, please see go/sysui-arch. \ + NOTE: if your code is not using Clean Architecture, please feel free to ignore \ + this warning. + """, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + CleanArchitectureDependencyViolationDetector::class.java, + Scope.JAVA_FILE_SCOPE, + ), + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 3f334c1cdb9c..254a6fb4714f 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -27,9 +27,11 @@ import com.google.auto.service.AutoService class SystemUIIssueRegistry : IssueRegistry() { override val issues: List<Issue> - get() = listOf( + get() = + listOf( BindServiceOnMainThreadDetector.ISSUE, BroadcastSentViaContextDetector.ISSUE, + CleanArchitectureDependencyViolationDetector.ISSUE, SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY, SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY, NonInjectedMainThreadDetector.ISSUE, @@ -37,7 +39,7 @@ class SystemUIIssueRegistry : IssueRegistry() { SoftwareBitmapDetector.ISSUE, NonInjectedServiceDetector.ISSUE, StaticSettingsProviderDetector.ISSUE - ) + ) override val api: Int get() = CURRENT_API @@ -45,9 +47,9 @@ class SystemUIIssueRegistry : IssueRegistry() { get() = 8 override val vendor: Vendor = - Vendor( - vendorName = "Android", - feedbackUrl = "http://b/issues/new?component=78010", - contact = "jernej@google.com" - ) + Vendor( + vendorName = "Android", + feedbackUrl = "http://b/issues/new?component=78010", + contact = "jernej@google.com" + ) } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt new file mode 100644 index 000000000000..a4b59fd8e086 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Ignore +import org.junit.Test + +@Suppress("UnstableApiUsage") +@Ignore("b/254533331") +class CleanArchitectureDependencyViolationDetectorTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector { + return CleanArchitectureDependencyViolationDetector() + } + + override fun getIssues(): List<Issue> { + return listOf( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + } + + @Test + fun `No violations`() { + lint() + .files( + *LEGITIMATE_FILES, + ) + .issues( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + .run() + .expectWarningCount(0) + } + + @Test + fun `Violation - domain depends on ui`() { + lint() + .files( + *LEGITIMATE_FILES, + TestFiles.kotlin( + """ + package test.domain.interactor + + import test.ui.viewmodel.ViewModel + + class BadClass( + private val viewModel: ViewModel, + ) + """.trimIndent() + ) + ) + .issues( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expect( + expectedText = + """ + src/test/domain/interactor/BadClass.kt:3: Warning: The domain layer may not depend on the ui layer. [CleanArchitectureDependencyViolation] + import test.ui.viewmodel.ViewModel + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """, + ) + } + + @Test + fun `Violation - ui depends on data`() { + lint() + .files( + *LEGITIMATE_FILES, + TestFiles.kotlin( + """ + package test.ui.viewmodel + + import test.data.repository.Repository + + class BadClass( + private val repository: Repository, + ) + """.trimIndent() + ) + ) + .issues( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expect( + expectedText = + """ + src/test/ui/viewmodel/BadClass.kt:3: Warning: The ui layer may not depend on the data layer. [CleanArchitectureDependencyViolation] + import test.data.repository.Repository + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """, + ) + } + + @Test + fun `Violation - shared depends on all other layers`() { + lint() + .files( + *LEGITIMATE_FILES, + TestFiles.kotlin( + """ + package test.shared.model + + import test.data.repository.Repository + import test.domain.interactor.Interactor + import test.ui.viewmodel.ViewModel + + class BadClass( + private val repository: Repository, + private val interactor: Interactor, + private val viewmodel: ViewModel, + ) + """.trimIndent() + ) + ) + .issues( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(3) + .expect( + expectedText = + """ + src/test/shared/model/BadClass.kt:3: Warning: The shared layer may not depend on the data layer. [CleanArchitectureDependencyViolation] + import test.data.repository.Repository + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/shared/model/BadClass.kt:4: Warning: The shared layer may not depend on the domain layer. [CleanArchitectureDependencyViolation] + import test.domain.interactor.Interactor + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/shared/model/BadClass.kt:5: Warning: The shared layer may not depend on the ui layer. [CleanArchitectureDependencyViolation] + import test.ui.viewmodel.ViewModel + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 3 warnings + """, + ) + } + + @Test + fun `Violation - data depends on domain`() { + lint() + .files( + *LEGITIMATE_FILES, + TestFiles.kotlin( + """ + package test.data.repository + + import test.domain.interactor.Interactor + + class BadClass( + private val interactor: Interactor, + ) + """.trimIndent() + ) + ) + .issues( + CleanArchitectureDependencyViolationDetector.ISSUE, + ) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expect( + expectedText = + """ + src/test/data/repository/BadClass.kt:3: Warning: The data layer may not depend on the domain layer. [CleanArchitectureDependencyViolation] + import test.domain.interactor.Interactor + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """, + ) + } + + companion object { + private val MODEL_FILE = + TestFiles.kotlin( + """ + package test.shared.model + + import test.some.other.thing.SomeOtherThing + + data class Model( + private val name: String, + ) + """.trimIndent() + ) + private val REPOSITORY_FILE = + TestFiles.kotlin( + """ + package test.data.repository + + import test.shared.model.Model + import test.some.other.thing.SomeOtherThing + + class Repository { + private val models = listOf( + Model("one"), + Model("two"), + Model("three"), + ) + + fun getModels(): List<Model> { + return models + } + } + """.trimIndent() + ) + private val INTERACTOR_FILE = + TestFiles.kotlin( + """ + package test.domain.interactor + + import test.data.repository.Repository + import test.shared.model.Model + + class Interactor( + private val repository: Repository, + ) { + fun getModels(): List<Model> { + return repository.getModels() + } + } + """.trimIndent() + ) + private val VIEW_MODEL_FILE = + TestFiles.kotlin( + """ + package test.ui.viewmodel + + import test.domain.interactor.Interactor + import test.some.other.thing.SomeOtherThing + + class ViewModel( + private val interactor: Interactor, + ) { + fun getNames(): List<String> { + return interactor.getModels().map { model -> model.name } + } + } + """.trimIndent() + ) + private val NON_CLEAN_ARCHITECTURE_FILE = + TestFiles.kotlin( + """ + package test.some.other.thing + + import test.data.repository.Repository + import test.domain.interactor.Interactor + import test.ui.viewmodel.ViewModel + + class SomeOtherThing { + init { + val viewModel = ViewModel( + interactor = Interactor( + repository = Repository(), + ), + ) + } + } + """.trimIndent() + ) + private val LEGITIMATE_FILES = + arrayOf( + MODEL_FILE, + REPOSITORY_FILE, + INTERACTOR_FILE, + VIEW_MODEL_FILE, + NON_CLEAN_ARCHITECTURE_FILE, + ) + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt index 259f0ed5c7a1..4e96ddabfda6 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext @@ -73,6 +74,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.lifecycle.ViewTreeViewModelStoreOwner +import com.android.compose.runtime.movableContentOf import com.android.systemui.animation.Expandable import com.android.systemui.animation.LaunchAnimator import kotlin.math.max @@ -170,25 +172,25 @@ fun Expandable( val contentColor = controller.contentColor val shape = controller.shape - // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the - // Compose libraries have been updated and include aosp/2163631. val wrappedContent = - @Composable { controller: ExpandableController -> - CompositionLocalProvider( - LocalContentColor provides contentColor, - ) { - // We make sure that the content itself (wrapped by the background) is at least - // 40.dp, which is the same as the M3 buttons. This applies even if onClick is - // null, to make it easier to write expandables that are sometimes clickable and - // sometimes not. There shouldn't be any Expandable smaller than 40dp because if - // the expandable is not clickable directly, then something in its content should - // be (and with a size >= 40dp). - val minSize = 40.dp - Box( - Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize), - contentAlignment = Alignment.Center, + remember(content) { + movableContentOf { expandable: Expandable -> + CompositionLocalProvider( + LocalContentColor provides contentColor, ) { - content(controller.expandable) + // We make sure that the content itself (wrapped by the background) is at least + // 40.dp, which is the same as the M3 buttons. This applies even if onClick is + // null, to make it easier to write expandables that are sometimes clickable and + // sometimes not. There shouldn't be any Expandable smaller than 40dp because if + // the expandable is not clickable directly, then something in its content + // should be (and with a size >= 40dp). + val minSize = 40.dp + Box( + Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize), + contentAlignment = Alignment.Center, + ) { + content(expandable) + } } } } @@ -270,7 +272,7 @@ fun Expandable( .onGloballyPositioned { controller.boundsInComposeViewRoot.value = it.boundsInRoot() } - ) { wrappedContent(controller) } + ) { wrappedContent(controller.expandable) } } else -> { val clickModifier = @@ -301,7 +303,7 @@ fun Expandable( controller.boundsInComposeViewRoot.value = it.boundsInRoot() }, ) { - wrappedContent(controller) + wrappedContent(controller.expandable) } } } @@ -315,7 +317,7 @@ private fun AnimatedContentInOverlay( animatorState: State<LaunchAnimator.State?>, overlay: ViewGroupOverlay, controller: ExpandableControllerImpl, - content: @Composable (ExpandableController) -> Unit, + content: @Composable (Expandable) -> Unit, composeViewRoot: View, onOverlayComposeViewChanged: (View?) -> Unit, density: Density, @@ -370,7 +372,7 @@ private fun AnimatedContentInOverlay( // We center the content in the expanding container. contentAlignment = Alignment.Center, ) { - Box(contentModifier) { content(controller) } + Box(contentModifier) { content(controller.expandable) } } } } diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 6e728ce7248f..e253fb925ceb 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -17,13 +17,21 @@ package com.android.systemui.compose +import android.content.Context +import android.view.View import androidx.activity.ComponentActivity +import androidx.lifecycle.LifecycleOwner import com.android.systemui.people.ui.viewmodel.PeopleViewModel +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel /** The Compose facade, when Compose is *not* available. */ object ComposeFacade : BaseComposeFacade { override fun isComposeAvailable(): Boolean = false + override fun composeInitializer(): ComposeInitializer { + throwComposeUnavailableError() + } + override fun setPeopleSpaceActivityContent( activity: ComponentActivity, viewModel: PeopleViewModel, @@ -32,7 +40,15 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } - private fun throwComposeUnavailableError() { + override fun createFooterActionsView( + context: Context, + viewModel: FooterActionsViewModel, + qsVisibilityLifecycleOwner: LifecycleOwner + ): View { + throwComposeUnavailableError() + } + + private fun throwComposeUnavailableError(): Nothing { error( "Compose is not available. Make sure to check isComposeAvailable() before calling any" + " other function on ComposeFacade." diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 6991ff82c2d1..1ea18fec4abe 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -16,16 +16,24 @@ package com.android.systemui.compose +import android.content.Context +import android.view.View import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner import com.android.compose.theme.PlatformTheme import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel +import com.android.systemui.qs.footer.ui.compose.FooterActions +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel /** The Compose facade, when Compose is available. */ object ComposeFacade : BaseComposeFacade { override fun isComposeAvailable(): Boolean = true + override fun composeInitializer(): ComposeInitializer = ComposeInitializerImpl + override fun setPeopleSpaceActivityContent( activity: ComponentActivity, viewModel: PeopleViewModel, @@ -33,4 +41,14 @@ object ComposeFacade : BaseComposeFacade { ) { activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } } } + + override fun createFooterActionsView( + context: Context, + viewModel: FooterActionsViewModel, + qsVisibilityLifecycleOwner: LifecycleOwner, + ): View { + return ComposeView(context).apply { + setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } } + } + } } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt new file mode 100644 index 000000000000..772c8918fd2d --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import com.android.compose.animation.ViewTreeSavedStateRegistryOwner +import com.android.systemui.lifecycle.ViewLifecycleOwner + +internal object ComposeInitializerImpl : ComposeInitializer { + override fun onAttachedToWindow(root: View) { + if (ViewTreeLifecycleOwner.get(root) != null) { + error("root $root already has a LifecycleOwner") + } + + val parent = root.parent + if (parent is View && parent.id != android.R.id.content) { + error( + "ComposeInitializer.onAttachedToWindow(View) must be called on the content child." + + "Outside of activities and dialogs, this is usually the top-most View of a " + + "window." + ) + } + + // The lifecycle owner, which is STARTED when [root] is visible and RESUMED when [root] is + // both visible and focused. + val lifecycleOwner = ViewLifecycleOwner(root) + + // We create a trivial implementation of [SavedStateRegistryOwner] that does not do any save + // or restore because SystemUI process is always running and top-level windows using this + // initializer are created once, when the process is started. + val savedStateRegistryOwner = + object : SavedStateRegistryOwner { + private val savedStateRegistry = + SavedStateRegistryController.create(this).apply { performRestore(null) } + + override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle + + override fun getSavedStateRegistry(): SavedStateRegistry { + return savedStateRegistry.savedStateRegistry + } + } + + // We must call [ViewLifecycleOwner.onCreate] after creating the [SavedStateRegistryOwner] + // because `onCreate` might move the lifecycle state to STARTED which will make + // [SavedStateRegistryController.performRestore] throw. + lifecycleOwner.onCreate() + + // Set the owners on the root. They will be reused by any ComposeView inside the root + // hierarchy. + ViewTreeLifecycleOwner.set(root, lifecycleOwner) + ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner) + } + + override fun onDetachedFromWindow(root: View) { + (ViewTreeLifecycleOwner.get(root) as ViewLifecycleOwner).onDestroy() + ViewTreeLifecycleOwner.set(root, null) + ViewTreeSavedStateRegistryOwner.set(root, null) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt new file mode 100644 index 000000000000..9eb78e14ab4e --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose.modifiers + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId + +/** + * Set a test tag on this node so that it is associated with [resId]. This node will then be + * accessible by integration tests using `sysuiResSelector(resId)`. + */ +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.sysuiResTag(resId: String): Modifier { + return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId") +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt index 23dacf9946f3..3eeadae5385f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt @@ -51,6 +51,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.R +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel @@ -110,7 +111,9 @@ private fun PeopleScreenWithConversations( recentTiles: List<PeopleTileViewModel>, onTileClicked: (PeopleTileViewModel) -> Unit, ) { - Column { + Column( + Modifier.sysuiResTag("top_level_with_conversations"), + ) { Column( Modifier.fillMaxWidth().padding(PeopleSpacePadding), horizontalAlignment = Alignment.CenterHorizontally, @@ -132,7 +135,7 @@ private fun PeopleScreenWithConversations( } LazyColumn( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().sysuiResTag("scroll_view"), contentPadding = PaddingValues( top = 16.dp, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 5c5ceefbd6fb..349f5c333116 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -73,6 +73,7 @@ import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel @@ -180,9 +181,9 @@ fun FooterActions( security?.let { SecurityButton(it, Modifier.weight(1f)) } foregroundServices?.let { ForegroundServicesButton(it) } - userSwitcher?.let { IconButton(it) } - IconButton(viewModel.settings) - viewModel.power?.let { IconButton(it) } + userSwitcher?.let { IconButton(it, Modifier.sysuiResTag("multi_user_switch")) } + IconButton(viewModel.settings, Modifier.sysuiResTag("settings_button_container")) + viewModel.power?.let { IconButton(it, Modifier.sysuiResTag("pm_lite")) } } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 462b90a10aee..86bd5f2bff5a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -54,7 +54,6 @@ class AnimatableClockView @JvmOverloads constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - var tag: String = "UnnamedClockView" var logBuffer: LogBuffer? = null private val time = Calendar.getInstance() @@ -132,7 +131,7 @@ class AnimatableClockView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - logBuffer?.log(tag, DEBUG, "onAttachedToWindow") + logBuffer?.log(TAG, DEBUG, "onAttachedToWindow") refreshFormat() } @@ -148,7 +147,7 @@ class AnimatableClockView @JvmOverloads constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: new formattedText=$str1" } ) @@ -157,7 +156,7 @@ class AnimatableClockView @JvmOverloads constructor( // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: done setting new time text to: $str1" } ) @@ -167,17 +166,17 @@ class AnimatableClockView @JvmOverloads constructor( // without being notified TextInterpolator being notified. if (layout != null) { textAnimator?.updateLayout(layout) - logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout") + logBuffer?.log(TAG, DEBUG, "refreshTime: done updating textAnimator layout") } requestLayout() - logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout") + logBuffer?.log(TAG, DEBUG, "refreshTime: after requestLayout") } } fun onTimeZoneChanged(timeZone: TimeZone?) { time.timeZone = timeZone refreshFormat() - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = timeZone?.toString() }, { "onTimeZoneChanged newTimeZone=$str1" } ) @@ -194,7 +193,7 @@ class AnimatableClockView @JvmOverloads constructor( } else { animator.updateLayout(layout) } - logBuffer?.log(tag, DEBUG, "onMeasure") + logBuffer?.log(TAG, DEBUG, "onMeasure") } override fun onDraw(canvas: Canvas) { @@ -206,12 +205,12 @@ class AnimatableClockView @JvmOverloads constructor( } else { super.onDraw(canvas) } - logBuffer?.log(tag, DEBUG, "onDraw lastDraw") + logBuffer?.log(TAG, DEBUG, "onDraw") } override fun invalidate() { super.invalidate() - logBuffer?.log(tag, DEBUG, "invalidate") + logBuffer?.log(TAG, DEBUG, "invalidate") } override fun onTextChanged( @@ -221,7 +220,7 @@ class AnimatableClockView @JvmOverloads constructor( lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = text.toString() }, { "onTextChanged text=$str1" } ) @@ -238,7 +237,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateColorChange() { - logBuffer?.log(tag, DEBUG, "animateColorChange") + logBuffer?.log(TAG, DEBUG, "animateColorChange") setTextStyle( weight = lockScreenWeight, textSize = -1f, @@ -260,7 +259,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateAppearOnLockscreen() { - logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen") + logBuffer?.log(TAG, DEBUG, "animateAppearOnLockscreen") setTextStyle( weight = dozingWeight, textSize = -1f, @@ -285,7 +284,7 @@ class AnimatableClockView @JvmOverloads constructor( if (isAnimationEnabled && textAnimator == null) { return } - logBuffer?.log(tag, DEBUG, "animateFoldAppear") + logBuffer?.log(TAG, DEBUG, "animateFoldAppear") setTextStyle( weight = lockScreenWeightInternal, textSize = -1f, @@ -312,7 +311,7 @@ class AnimatableClockView @JvmOverloads constructor( // Skip charge animation if dozing animation is already playing. return } - logBuffer?.log(tag, DEBUG, "animateCharge") + logBuffer?.log(TAG, DEBUG, "animateCharge") val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -336,7 +335,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - logBuffer?.log(tag, DEBUG, "animateDoze") + logBuffer?.log(TAG, DEBUG, "animateDoze") setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, @@ -455,7 +454,7 @@ class AnimatableClockView @JvmOverloads constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = format?.toString() }, { "refreshFormat format=$str1" } ) @@ -466,6 +465,7 @@ class AnimatableClockView @JvmOverloads constructor( fun dump(pw: PrintWriter) { pw.println("$this") + pw.println(" alpha=$alpha") pw.println(" measuredWidth=$measuredWidth") pw.println(" measuredHeight=$measuredHeight") pw.println(" singleLineInternal=$isSingleLineInternal") @@ -626,7 +626,7 @@ class AnimatableClockView @JvmOverloads constructor( } companion object { - private val TAG = AnimatableClockView::class.simpleName + private val TAG = AnimatableClockView::class.simpleName!! const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600 private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index e138ef8a1ea8..7645decfde24 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -88,13 +88,6 @@ class DefaultClockController( events.onTimeTick() } - override fun setLogBuffer(logBuffer: LogBuffer) { - smallClock.view.tag = "smallClockView" - largeClock.view.tag = "largeClockView" - smallClock.view.logBuffer = logBuffer - largeClock.view.logBuffer = logBuffer - } - open inner class DefaultClockFaceController( override val view: AnimatableClockView, ) : ClockFaceController { @@ -104,6 +97,12 @@ class DefaultClockController( private var isRegionDark = false protected var targetRegion: Rect? = null + override var logBuffer: LogBuffer? + get() = view.logBuffer + set(value) { + view.logBuffer = value + } + init { view.setColors(currentColor, currentColor) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt index 5bb37071b075..cd9fb886a4e6 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt @@ -20,12 +20,15 @@ package com.android.systemui.shared.customization.data.content import android.annotation.SuppressLint import android.content.ContentValues import android.content.Context +import android.content.Intent import android.database.ContentObserver import android.graphics.Color import android.graphics.drawable.Drawable import android.net.Uri +import android.util.Log import androidx.annotation.DrawableRes import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract +import java.net.URISyntaxException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -169,6 +172,8 @@ interface CustomizationProviderClient { * If `null`, the button should not be shown. */ val enablementActionComponentName: String? = null, + /** Optional [Intent] to use to start an activity to configure this affordance. */ + val configureIntent: Intent? = null, ) /** Models a selection of a quick affordance on a slot. */ @@ -337,6 +342,11 @@ class CustomizationProviderClientImpl( Contract.LockScreenQuickAffordances.AffordanceTable.Columns .ENABLEMENT_COMPONENT_NAME ) + val configureIntentColumnIndex = + cursor.getColumnIndex( + Contract.LockScreenQuickAffordances.AffordanceTable.Columns + .CONFIGURE_INTENT + ) if ( idColumnIndex == -1 || nameColumnIndex == -1 || @@ -344,15 +354,17 @@ class CustomizationProviderClientImpl( isEnabledColumnIndex == -1 || enablementInstructionsColumnIndex == -1 || enablementActionTextColumnIndex == -1 || - enablementComponentNameColumnIndex == -1 + enablementComponentNameColumnIndex == -1 || + configureIntentColumnIndex == -1 ) { return@buildList } while (cursor.moveToNext()) { + val affordanceId = cursor.getString(idColumnIndex) add( CustomizationProviderClient.Affordance( - id = cursor.getString(idColumnIndex), + id = affordanceId, name = cursor.getString(nameColumnIndex), iconResourceId = cursor.getInt(iconColumnIndex), isEnabled = cursor.getInt(isEnabledColumnIndex) == 1, @@ -367,6 +379,10 @@ class CustomizationProviderClientImpl( cursor.getString(enablementActionTextColumnIndex), enablementActionComponentName = cursor.getString(enablementComponentNameColumnIndex), + configureIntent = + cursor + .getString(configureIntentColumnIndex) + ?.toIntent(affordanceId = affordanceId), ) ) } @@ -504,7 +520,19 @@ class CustomizationProviderClientImpl( .onStart { emit(Unit) } } + private fun String.toIntent( + affordanceId: String, + ): Intent? { + return try { + Intent.parseUri(this, 0) + } catch (e: URISyntaxException) { + Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!") + null + } + } + companion object { + private const val TAG = "CustomizationProviderClient" private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt index 1e2e7d2595ac..7f1c78fc47ff 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt @@ -113,6 +113,11 @@ object CustomizationProviderContract { * opens a destination where the user can re-enable the disabled affordance. */ const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent" + /** + * Byte array. Optional parcelled `Intent` to use to start an activity that can be + * used to configure the affordance. + */ + const val CONFIGURE_INTENT = "configure_intent" } } diff --git a/packages/SystemUI/docs/dagger.md b/packages/SystemUI/docs/dagger.md index 89170139e21c..9b4c21efb27f 100644 --- a/packages/SystemUI/docs/dagger.md +++ b/packages/SystemUI/docs/dagger.md @@ -8,105 +8,110 @@ Go read about Dagger 2. - [User's guide](https://google.github.io/dagger/users-guide) -TODO: Add some links. - ## State of the world -Dagger 2 has been turned on for SystemUI and a early first pass has been taken -for converting everything in [Dependency.java](packages/systemui/src/com/android/systemui/Dependency.java) -to use Dagger. Since a lot of SystemUI depends on Dependency, stubs have been added to Dependency -to proxy any gets through to the instances provided by dagger, this will allow migration of SystemUI -through a number of CLs. +Dagger 2 has been turned on for SystemUI and much of +[Dependency.java](../src/com/android/systemui/Dependency.java) +has been converted to use Dagger. Since a lot of SystemUI depends on Dependency, +stubs have been added to Dependency to proxy any gets through to the instances +provided by dagger, this will allow migration of SystemUI through a number of CLs. ### How it works in SystemUI +There are three high level "scopes" of concern in SystemUI. They all represent +singleton scopes, but serve different purposes. + +* `@Singleton` - Instances that are shared everywhere. There isn't a lot of + code in this scope. Things like the main thread, and Android Framework + provided instances mostly. +* `@WMShell` - WindowManager related code in the SystemUI process. We don't + want this code relying on the rest of SystemUI, and we don't want the rest + of SystemUI peeking into its internals, so it runs in its own Subcomponent. +* `@SysUISingleton` - Most of what would be considered "SystemUI". Most feature + work by SystemUI developers goes into this scope. Useful interfaces from + WindowManager are made available inside this Subcomponent. + +The root dagger graph is created by an instance of `SystemUIInitializer`. +See [README.md](../README.md) for more details. For the classes that we're using in Dependency and are switching to dagger, the equivalent dagger version is using `@Singleton` and therefore only has one instance. To have the single instance span all of SystemUI and be easily accessible for other components, there is a single root `@Component` that exists that generates -these. The component lives in [SystemUIFactory](packages/systemui/src/com/android/systemui/SystemUIFactory.java) -and is called `SystemUIRootComponent`. +these. The component lives in +[ReferenceGlobalRootComponent.java](../src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java). -```java +### Adding a new injectable object -@Singleton -@Component(modules = {SystemUIFactory.class, DependencyProvider.class, DependencyBinder.class, - ContextHolder.class}) -public interface SystemUIRootComponent { - @Singleton - Dependency.DependencyInjector createDependency(); +First annotate the constructor with `@Inject`. Also annotate it with +`@SysUISingleton` if only one instance should be created. + +```kotlin +@SysUISingleton +class FeatureStartable +@Inject +constructor( +/* ... */ +) { + // ... } ``` -The root component is composed of root modules, which in turn provide the global singleton -dependencies across all of SystemUI. - -- `SystemUIFactory` `@Provides` dependencies that need to be overridden by SystemUI -variants (like other form factors e.g. Car). - -- `DependencyBinder` creates the mapping from interfaces to implementation classes. +If you have an interface class and an implementation class, Dagger needs to +know how to map it. The simplest way to do this is to add an `@Binds` method +in a module. The type of the return value tells dagger which dependency it's +providing: -- `DependencyProvider` provides or binds any remaining depedencies required. - -### Adding injection to a new SystemUI object - -SystemUI object are made injectable by adding an entry in `SystemUIBinder`. SystemUIApplication uses -information in that file to locate and construct an instance of the requested SystemUI class. - -### Adding a new injectable object - -First tag the constructor with `@Inject`. Also tag it with `@Singleton` if only one -instance should be created. - -```java -@Singleton -public class SomethingController { - @Inject - public SomethingController(Context context, - @Named(MAIN_HANDLER_NAME) Handler mainHandler) { - // context and mainHandler will be automatically populated. - } +```kotlin +@Module +abstract class FeatureModule { + @Binds + abstract fun bindsFeature(impl: FeatureImpl): Feature } ``` -If you have an interface class and an implementation class, dagger needs to know -how to map it. The simplest way to do this is to add an `@Provides` method to -DependencyProvider. The type of the return value tells dagger which dependency it's providing. - -```java -public class DependencyProvider { - //... - @Singleton - @Provides - public SomethingController provideSomethingController(Context context, - @Named(MAIN_HANDLER_NAME) Handler mainHandler) { - return new SomethingControllerImpl(context, mainHandler); - } +If you have a class that you want to make injectable that has can not +be easily constructed by Dagger, write a `@Provides` method for it: + +```kotlin +@Module +abstract class FeatureModule { + @Module + companion object { + @Provides + fun providesFeature(ctx: Context): Feature { + return FeatureImpl.constructFromContext(ctx) + } + } } ``` -If you need to access this from Dependency#get, then add an adapter to Dependency -that maps to the instance provided by Dagger. The changes should be similar -to the following diff. +### Module Organization -```java -public class Dependency { - //... - @Inject Lazy<SomethingController> mSomethingController; - //... - public void start() { - //... - mProviders.put(SomethingController.class, mSomethingController::get); - } -} -``` +Please define your modules on _at least_ per-package level. If the scope of a +package grows to encompass a great number of features, create per-feature +modules. + +**Do not create catch-all modules.** Those quickly grow unwieldy and +unmaintainable. Any that exist today should be refactored into obsolescence. + +You can then include your module in one of three places: + +1) Within another module that depends on it. Ideally, this creates a clean + dependency graph between features and utilities. +2) For features that should exist in all versions of SystemUI (AOSP and + any variants), include the module in + [SystemUIModule.java](../src/com/android/systemui/dagger/SystemUIModule.java). +3) For features that should exist only in AOSP, include the module in + [ReferenceSystemUIModule.java](../src/com/android/systemui/dagger/ReferenceSystemUIModule.java). + Similarly, if you are working on a custom version of SystemUI and have code + specific to your version, include it in a module specific to your version. ### Using injection with Fragments Fragments are created as part of the FragmentManager, so they need to be setup so the manager knows how to create them. To do that, add a method to com.android.systemui.fragments.FragmentService$FragmentCreator that -returns your fragment class. Thats all thats required, once the method +returns your fragment class. That is all that is required, once the method exists, FragmentService will automatically pick it up and use injection whenever your fragment needs to be created. @@ -123,48 +128,11 @@ then the FragmentHostManager can do this for you. FragmentHostManager.get(view).create(NavigationBarFragment.class); ``` -### Using injection with Views - -DO NOT ADD NEW VIEW INJECTION. VIEW INJECTION IS BEING ACTIVELY DEPRECATED. - -Needing to inject objects into your View's constructor generally implies you -are doing more work in your presentation layer than is advisable. -Instead, create an injected controller for you view, inject into the -controller, and then attach the view to the controller after inflation. - -View injection generally causes headaches while testing, as inflating a view -(which may in turn inflate other views) implicitly causes a Dagger graph to -be stood up, which may or may not contain the appropriately -faked/mocked/stubbed objects. It is a hard to control process. - ## Updating Dagger2 We depend on the Dagger source found in external/dagger2. We should automatically pick up on updates when that repository is updated. - -*Deprecated:* - -Binaries can be downloaded from https://repo1.maven.org/maven2/com/google/dagger/ and then loaded -into -[/prebuilts/tools/common/m2/repository/com/google/dagger/](http://cs/android/prebuilts/tools/common/m2/repository/com/google/dagger/) - -The following commands should work, substituting in the version that you are looking for: - -```` -cd prebuilts/tools/common/m2/repository/com/google/dagger/ - -wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger/2.28.1/ - -wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.28.1/ - -wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-spi/2.28.1/ - -wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.28.1/ -```` - -Then update `prebuilts/tools/common/m2/Android.bp` to point at your new jars. ## TODO List - - Eliminate usages of Dependency#get - - Add links in above TODO + - Eliminate usages of Dependency#get: http://b/hotlists/3940788 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 dee0f5cd1979..314c736e2e44 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -80,6 +80,7 @@ internal class HueSubtract(val amountDegrees: Double) : Hue { internal class HueVibrantSecondary() : Hue { val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12), Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12)) + override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) } @@ -88,6 +89,7 @@ internal class HueVibrantSecondary() : Hue { internal class HueVibrantTertiary() : Hue { val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25), Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25)) + override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) } @@ -96,6 +98,7 @@ internal class HueVibrantTertiary() : Hue { internal class HueExpressiveSecondary() : Hue { val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20), Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45)) + override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) } @@ -104,6 +107,7 @@ internal class HueExpressiveSecondary() : Hue { internal class HueExpressiveTertiary() : Hue { val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45), Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120)) + override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) } @@ -148,11 +152,11 @@ internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { } internal class CoreSpec( - val a1: TonalSpec, - val a2: TonalSpec, - val a3: TonalSpec, - val n1: TonalSpec, - val n2: TonalSpec + val a1: TonalSpec, + val a2: TonalSpec, + val a3: TonalSpec, + val n1: TonalSpec, + val n2: TonalSpec ) enum class Style(internal val coreSpec: CoreSpec) { @@ -214,51 +218,86 @@ enum class Style(internal val coreSpec: CoreSpec) { )), } +class TonalPalette { + val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) + val allShades: List<Int> + val allShadesMapped: Map<Int, Int> + val baseColor: Int + + internal constructor(spec: TonalSpec, seedColor: Int) { + val seedCam = Cam.fromInt(seedColor) + allShades = spec.shades(seedCam) + allShadesMapped = shadeKeys.zip(allShades).toMap() + + val h = spec.hue.get(seedCam).toFloat() + val c = spec.chroma.get(seedCam).toFloat() + baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor)) + } + + val s10: Int get() = this.allShades[0] + val s50: Int get() = this.allShades[1] + val s100: Int get() = this.allShades[2] + val s200: Int get() = this.allShades[3] + val s300: Int get() = this.allShades[4] + val s400: Int get() = this.allShades[5] + val s500: Int get() = this.allShades[6] + val s600: Int get() = this.allShades[7] + val s700: Int get() = this.allShades[8] + val s800: Int get() = this.allShades[9] + val s900: Int get() = this.allShades[10] + val s1000: Int get() = this.allShades[11] +} + class ColorScheme( - @ColorInt val seed: Int, - val darkTheme: Boolean, - val style: Style = Style.TONAL_SPOT + @ColorInt val seed: Int, + val darkTheme: Boolean, + val style: Style = Style.TONAL_SPOT ) { - val accent1: List<Int> - val accent2: List<Int> - val accent3: List<Int> - val neutral1: List<Int> - val neutral2: List<Int> + val accent1: TonalPalette + val accent2: TonalPalette + val accent3: TonalPalette + val neutral1: TonalPalette + val neutral2: TonalPalette constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads constructor( - wallpaperColors: WallpaperColors, - darkTheme: Boolean, - style: Style = Style.TONAL_SPOT + wallpaperColors: WallpaperColors, + darkTheme: Boolean, + style: Style = Style.TONAL_SPOT ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) + val allHues: List<TonalPalette> + get() { + return listOf(accent1, accent2, accent3, neutral1, neutral2) + } + val allAccentColors: List<Int> get() { val allColors = mutableListOf<Int>() - allColors.addAll(accent1) - allColors.addAll(accent2) - allColors.addAll(accent3) + allColors.addAll(accent1.allShades) + allColors.addAll(accent2.allShades) + allColors.addAll(accent3.allShades) return allColors } val allNeutralColors: List<Int> get() { val allColors = mutableListOf<Int>() - allColors.addAll(neutral1) - allColors.addAll(neutral2) + allColors.addAll(neutral1.allShades) + allColors.addAll(neutral2.allShades) return allColors } val backgroundColor - get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1[8] else neutral1[0], 0xFF) + get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1.s700 else neutral1.s10, 0xFF) val accentColor - get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF) + get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1.s100 else accent1.s500, 0xFF) init { val proposedSeedCam = Cam.fromInt(seed) @@ -269,24 +308,26 @@ class ColorScheme( } else { seed } - val camSeed = Cam.fromInt(seedArgb) - accent1 = style.coreSpec.a1.shades(camSeed) - accent2 = style.coreSpec.a2.shades(camSeed) - accent3 = style.coreSpec.a3.shades(camSeed) - neutral1 = style.coreSpec.n1.shades(camSeed) - neutral2 = style.coreSpec.n2.shades(camSeed) + + accent1 = TonalPalette(style.coreSpec.a1, seedArgb) + accent2 = TonalPalette(style.coreSpec.a2, seedArgb) + accent3 = TonalPalette(style.coreSpec.a3, seedArgb) + neutral1 = TonalPalette(style.coreSpec.n1, seedArgb) + neutral2 = TonalPalette(style.coreSpec.n2, seedArgb) } + val shadeCount get() = this.accent1.allShades.size + override fun toString(): String { return "ColorScheme {\n" + " seed color: ${stringForColor(seed)}\n" + " style: $style\n" + " palettes: \n" + - " ${humanReadable("PRIMARY", accent1)}\n" + - " ${humanReadable("SECONDARY", accent2)}\n" + - " ${humanReadable("TERTIARY", accent3)}\n" + - " ${humanReadable("NEUTRAL", neutral1)}\n" + - " ${humanReadable("NEUTRAL VARIANT", neutral2)}\n" + + " ${humanReadable("PRIMARY", accent1.allShades)}\n" + + " ${humanReadable("SECONDARY", accent2.allShades)}\n" + + " ${humanReadable("TERTIARY", accent3.allShades)}\n" + + " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + + " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + "}" } @@ -385,7 +426,8 @@ class ColorScheme( val existingSeedNearby = seeds.find { val hueA = intToCam[int]!!.hue val hueB = intToCam[it]!!.hue - hueDiff(hueA, hueB) < i } != null + hueDiff(hueA, hueB) < i + } != null if (existingSeedNearby) { continue } @@ -460,9 +502,9 @@ class ColorScheme( } private fun huePopulations( - camByColor: Map<Int, Cam>, - populationByColor: Map<Int, Double>, - filter: Boolean = true + camByColor: Map<Int, Cam>, + populationByColor: Map<Int, Double>, + filter: Boolean = true ): List<Double> { val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 7709f210f22f..fb1c454de70d 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -29,6 +29,7 @@ java_library { "src/**/*.java", "src/**/*.kt", "bcsmartspace/src/**/*.java", + "bcsmartspace/src/**/*.kt", ], static_libs: [ diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt new file mode 100644 index 000000000000..509f022310d0 --- /dev/null +++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.plugins + +// TODO(b/265360975): Evaluate this plugin approach. +/** Plugin to provide BC smartspace configuration */ +interface BcSmartspaceConfigPlugin { + /** Gets default date/weather disabled status. */ + val isDefaultDateWeatherDisabled: Boolean +} diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java index 51f5baa8b45f..bc6e5ec6117c 100644 --- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java +++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java @@ -94,6 +94,11 @@ public interface BcSmartspaceDataPlugin extends Plugin { void registerDataProvider(BcSmartspaceDataPlugin plugin); /** + * Sets {@link BcSmartspaceConfigPlugin}. + */ + void registerConfigProvider(BcSmartspaceConfigPlugin configProvider); + + /** * Primary color for unprotected text */ void setPrimaryTextColor(int color); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 66e44b9005de..a2a07095c16c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -71,9 +71,6 @@ interface ClockController { /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) {} - - /** Optional method for debug logging */ - fun setLogBuffer(logBuffer: LogBuffer) {} } /** Interface for a specific clock face version rendered by the clock */ @@ -83,6 +80,9 @@ interface ClockFaceController { /** Events specific to this clock face */ val events: ClockFaceEvents + + /** Some clocks may log debug information */ + var logBuffer: LogBuffer? } /** Events that should call when various rendering parameters change */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6436dcb5f613..e99b2149bc1d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -159,8 +159,13 @@ constructor( * bug report more actionable, so using the [log] with a messagePrinter to add more detail to * every log may do more to improve overall logging than adding more logs with this method. */ - fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, { str1 = message }, { str1!! }) + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(tag, level, { str1 = message }, { str1!! }, exception) /** * You should call [log] instead of this method. diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index f96644fbd667..5fc919330e92 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -16,6 +16,50 @@ public <init>(); } +# Needed to ensure callback field references are kept in their respective +# owning classes when the downstream callback registrars only store weak refs. +# TODO(b/264686688): Handle these cases with more targeted annotations. +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + private com.android.keyguard.KeyguardUpdateMonitorCallback *; + private com.android.systemui.privacy.PrivacyConfig$Callback *; + private com.android.systemui.privacy.PrivacyItemController$Callback *; + private com.android.systemui.settings.UserTracker$Callback *; + private com.android.systemui.statusbar.phone.StatusBarWindowCallback *; + private com.android.systemui.util.service.Observer$Callback *; + private com.android.systemui.util.service.ObservableServiceConnection$Callback *; +} +# Note that these rules are temporary companions to the above rules, required +# for cases like Kotlin where fields with anonymous types use the anonymous type +# rather than the supertype. +-if class * extends com.android.keyguard.KeyguardUpdateMonitorCallback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.privacy.PrivacyConfig$Callback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.privacy.PrivacyItemController$Callback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.settings.UserTracker$Callback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.statusbar.phone.StatusBarWindowCallback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.util.service.Observer$Callback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} +-if class * extends com.android.systemui.util.service.ObservableServiceConnection$Callback +-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** { + <1> *; +} + -keepclasseswithmembers class * { public <init>(android.content.Context, android.util.AttributeSet); } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 2b7bdc2dc4cb..c772c9649cc7 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -27,7 +27,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - androidprv:layout_maxWidth="@dimen/keyguard_security_width" + androidprv:layout_maxWidth="@dimen/biometric_auth_pattern_view_max_size" android:layout_gravity="center_horizontal|bottom" android:clipChildren="false" android:clipToPadding="false"> diff --git a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml b/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml deleted file mode 100644 index a3c37e420f29..000000000000 --- a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/apps/common/assets/res/any/dimens.xml -** -** Copyright 2013, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> -<resources> - <!-- Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">550dp</dimen> -</resources> diff --git a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml index 1dc61c501beb..b7a1bb46c317 100644 --- a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml @@ -17,10 +17,5 @@ */ --> <resources> - - <!-- Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">470dp</dimen> - <dimen name="widget_big_font_size">100dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index c5ffdc0051da..6cc5b9d7b7e8 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -29,9 +29,6 @@ (includes 2x keyguard_security_view_top_margin) --> <dimen name="keyguard_security_height">420dp</dimen> - <!-- Max Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <!-- pin/password field max height --> <dimen name="keyguard_password_height">80dp</dimen> diff --git a/packages/SystemUI/res/drawable/ic_camera.xml b/packages/SystemUI/res/drawable/ic_camera.xml new file mode 100644 index 000000000000..ef1406c1c58a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_camera.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M7,42Q5.8,42 4.9,41.1Q4,40.2 4,39V13.35Q4,12.15 4.9,11.25Q5.8,10.35 7,10.35H14.35L18,6H30L33.65,10.35H41Q42.2,10.35 43.1,11.25Q44,12.15 44,13.35V39Q44,40.2 43.1,41.1Q42.2,42 41,42ZM7,39H41Q41,39 41,39Q41,39 41,39V13.35Q41,13.35 41,13.35Q41,13.35 41,13.35H7Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM7,39Q7,39 7,39Q7,39 7,39V13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM24,34.7Q27.5,34.7 30,32.225Q32.5,29.75 32.5,26.2Q32.5,22.7 30,20.2Q27.5,17.7 24,17.7Q20.45,17.7 17.975,20.2Q15.5,22.7 15.5,26.2Q15.5,29.75 17.975,32.225Q20.45,34.7 24,34.7ZM24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml new file mode 100644 index 000000000000..08c5aaf56bf7 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml @@ -0,0 +1,26 @@ +<?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 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="13dp" + android:height="13dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_videocam.xml b/packages/SystemUI/res/drawable/ic_videocam.xml new file mode 100644 index 000000000000..de2bc7bccdf1 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_videocam.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M7,40Q5.8,40 4.9,39.1Q4,38.2 4,37V11Q4,9.8 4.9,8.9Q5.8,8 7,8H33Q34.2,8 35.1,8.9Q36,9.8 36,11V21.75L44,13.75V34.25L36,26.25V37Q36,38.2 35.1,39.1Q34.2,40 33,40ZM7,37H33Q33,37 33,37Q33,37 33,37V11Q33,11 33,11Q33,11 33,11H7Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37ZM7,37Q7,37 7,37Q7,37 7,37V11Q7,11 7,11Q7,11 7,11Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml index 857632edcf0d..53122c17e320 100644 --- a/packages/SystemUI/res/drawable/overlay_badge_background.xml +++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2020 The Android Open Source Project + ~ 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. @@ -14,8 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="oval"> - <solid android:color="?androidprv:attr/colorSurface"/> -</shape> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:pathData="M0,0M48,48"/> +</vector> 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 a3dd334bd667..3505a3e6b6bf 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -71,8 +71,8 @@ <com.android.internal.widget.LockPatternView android:id="@+id/lockPattern" android:layout_gravity="center" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> <TextView android:id="@+id/error" diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 4af997017bba..147ea8221beb 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -67,8 +67,8 @@ <com.android.internal.widget.LockPatternView android:id="@+id/lockPattern" android:layout_gravity="center" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> <TextView android:id="@+id/error" diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index bc97e511e7f4..8cf4f4de27da 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -23,6 +23,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> + <!-- Extra marginBottom to give room for the drop shadow. --> <LinearLayout android:id="@+id/chipbar_inner" android:orientation="horizontal" @@ -33,6 +34,8 @@ android:layout_marginTop="20dp" android:layout_marginStart="@dimen/notification_side_paddings" android:layout_marginEnd="@dimen/notification_side_paddings" + android:translationZ="4dp" + android:layout_marginBottom="8dp" android:clipToPadding="false" android:gravity="center_vertical" android:alpha="0.0" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 9134f96f59e1..eec3b11519b1 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -32,26 +32,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toBottomOf="parent"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" - android:layout_marginBottom="4dp" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/actions" android:layout_width="wrap_content" @@ -69,44 +69,30 @@ android:id="@+id/preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" - app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" - app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" - android:background="@drawable/overlay_border"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="clipboard_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="clipboard_preview"/> + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <FrameLayout android:id="@+id/clipboard_preview" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" android:elevation="7dp" android:background="@drawable/overlay_preview_background" android:clipChildren="true" android:clipToOutline="true" android:clipToPadding="true" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_margin="@dimen/overlay_border_width" - android:layout_height="wrap_content" - android:layout_gravity="center" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/preview_border" app:layout_constraintStart_toStartOf="@id/preview_border" - app:layout_constraintEnd_toEndOf="@id/preview_border" - app:layout_constraintTop_toTopOf="@id/preview_border"> + app:layout_constraintBottom_toBottomOf="@id/preview_border"> <TextView android:id="@+id/text_preview" android:textFontWeight="500" android:padding="8dp" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index a565988c14ad..d68982876448 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -148,9 +148,4 @@ <include layout="@layout/ongoing_privacy_chip"/> </FrameLayout> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:id="@+id/space" - /> </com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 9add32c6ee0a..885e5e2d4441 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -57,6 +57,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_alarm" android:tint="@android:color/white" android:visibility="gone" @@ -67,6 +68,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_qs_dnd_on" android:tint="@android:color/white" android:visibility="gone" @@ -77,6 +79,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_signal_wifi_off" android:visibility="gone" android:contentDescription="@string/dream_overlay_status_bar_wifi_off" /> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 95aefab328df..abc83379950a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -147,6 +147,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <!-- Explicit Indicator --> + <com.android.internal.widget.CachingIconView + android:id="@+id/media_explicit_indicator" + android:layout_width="@dimen/qs_media_explicit_indicator_icon_size" + android:layout_height="@dimen/qs_media_explicit_indicator_icon_size" + android:src="@drawable/ic_media_explicit_indicator" + /> + <!-- Artist name --> <TextView android:id="@+id/header_artist" diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml index 5aa608084510..d1a2cf4c24b2 100644 --- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -25,6 +25,7 @@ android:focusable="true" android:clipChildren="false" android:clipToPadding="false" + android:paddingStart="8dp" > <LinearLayout diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index e4e0bd45a2db..496eb6e6130e 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -27,26 +27,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="4dp" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/screenshot_actions" android:layout_width="wrap_content" @@ -64,35 +64,24 @@ android:id="@+id/screenshot_preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" android:alpha="0" android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="screenshot_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="screenshot_preview"/> + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <ImageView android:id="@+id/screenshot_preview" android:visibility="invisible" android:layout_width="@dimen/overlay_x_scale" - android:layout_margin="@dimen/overlay_border_width" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" android:layout_gravity="center" android:elevation="7dp" android:contentDescription="@string/screenshot_edit_description" @@ -100,20 +89,14 @@ android:background="@drawable/overlay_preview_background" android:adjustViewBounds="true" android:clickable="true" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/> + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> <ImageView android:id="@+id/screenshot_badge" - android:layout_width="24dp" - android:layout_height="24dp" - android:padding="4dp" + android:layout_width="48dp" + android:layout_height="48dp" android:visibility="gone" - android:background="@drawable/overlay_badge_background" android:elevation="8dp" - android:src="@drawable/overlay_cancel" app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> <FrameLayout @@ -150,7 +133,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" android:layout_marginVertical="4dp" - android:paddingHorizontal="@dimen/overlay_action_container_padding_right" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:background="@drawable/action_chip_container_background" diff --git a/packages/SystemUI/res/layout/status_bar_notification_footer.xml b/packages/SystemUI/res/layout/status_bar_notification_footer.xml index bbb8df1c5a4a..db94c92738f2 100644 --- a/packages/SystemUI/res/layout/status_bar_notification_footer.xml +++ b/packages/SystemUI/res/layout/status_bar_notification_footer.xml @@ -26,6 +26,17 @@ android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content"> + <TextView + android:id="@+id/unlock_prompt_footer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:drawablePadding="8dp" + android:visibility="gone" + android:textAppearance="?android:attr/textAppearanceButton" + android:text="@string/unlock_to_see_notif_text"/> <com.android.systemui.statusbar.notification.row.FooterViewButton style="@style/TextAppearance.NotificationSectionHeaderButton" android:id="@+id/manage_text" diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml index fa9d7390dcf8..7eaed4356f46 100644 --- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -46,7 +46,7 @@ app:layout_constraintEnd_toEndOf="parent" app:flow_horizontalBias="0.5" app:flow_verticalAlign="center" - app:flow_wrapMode="chain" + app:flow_wrapMode="chain2" app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" app:flow_verticalGap="44dp" app:flow_horizontalStyle="packed"/> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 49ef330dcc52..fff25448b2e4 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -40,6 +40,10 @@ <dimen name="biometric_dialog_button_negative_max_width">140dp</dimen> <dimen name="biometric_dialog_button_positive_max_width">116dp</dimen> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> + <dimen name="global_actions_power_dialog_item_height">130dp</dimen> <dimen name="global_actions_power_dialog_item_bottom_margin">35dp</dimen> diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index aefd9981d02e..a0e721e571d8 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -29,11 +29,11 @@ <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:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:paddingHorizontal">32dp</item> <item name="android:paddingVertical">20dp</item> </style> diff --git a/packages/SystemUI/res/values-sw360dp/dimens.xml b/packages/SystemUI/res/values-sw360dp/dimens.xml index 65ca70bac0dc..03365b3d8c4f 100644 --- a/packages/SystemUI/res/values-sw360dp/dimens.xml +++ b/packages/SystemUI/res/values-sw360dp/dimens.xml @@ -25,5 +25,8 @@ <!-- Home Controls --> <dimen name="global_actions_side_margin">12dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">298dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw392dp-land/dimens.xml b/packages/SystemUI/res/values-sw392dp-land/dimens.xml new file mode 100644 index 000000000000..1e26a699f85a --- /dev/null +++ b/packages/SystemUI/res/values-sw392dp-land/dimens.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">248dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw392dp/dimens.xml b/packages/SystemUI/res/values-sw392dp/dimens.xml index 78279ca4f520..96af3c13f32e 100644 --- a/packages/SystemUI/res/values-sw392dp/dimens.xml +++ b/packages/SystemUI/res/values-sw392dp/dimens.xml @@ -24,5 +24,8 @@ <!-- Home Controls --> <dimen name="global_actions_side_margin">16dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">298dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw410dp-land/dimens.xml b/packages/SystemUI/res/values-sw410dp-land/dimens.xml new file mode 100644 index 000000000000..c4d9b9b92f57 --- /dev/null +++ b/packages/SystemUI/res/values-sw410dp-land/dimens.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw410dp/dimens.xml b/packages/SystemUI/res/values-sw410dp/dimens.xml index 7da47e5089be..ff6e005a94c7 100644 --- a/packages/SystemUI/res/values-sw410dp/dimens.xml +++ b/packages/SystemUI/res/values-sw410dp/dimens.xml @@ -27,4 +27,6 @@ <dimen name="global_actions_grid_item_side_margin">12dp</dimen> <dimen name="global_actions_grid_item_height">72dp</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index 6c7cab51f440..5d78e4e1a44c 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -28,6 +28,7 @@ <!-- QS--> <dimen name="qs_panel_padding_top">16dp</dimen> + <dimen name="qs_panel_padding">24dp</dimen> <dimen name="qs_content_horizontal_padding">24dp</dimen> <dimen name="qs_horizontal_margin">24dp</dimen> <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2, diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml index 8148d3dfaf7d..c535c6416106 100644 --- a/packages/SystemUI/res/values-sw600dp-land/styles.xml +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -18,10 +18,10 @@ <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:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">120dp</item> <item name="android:paddingVertical">40dp</item> </style> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml index 771de08cb360..32eefa7316b2 100644 --- a/packages/SystemUI/res/values-sw600dp-port/styles.xml +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -26,10 +26,10 @@ <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:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">180dp</item> <item name="android:paddingVertical">80dp</item> </style> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 599bf30a5135..9bc0dde49b4c 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -92,4 +92,6 @@ <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml index 3fc59e38ec6c..122806a1af8f 100644 --- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml @@ -27,7 +27,7 @@ <dimen name="status_view_margin_horizontal">24dp</dimen> - <dimen name="qs_media_session_height_expanded">251dp</dimen> + <dimen name="qs_media_session_height_expanded">184dp</dimen> <dimen name="qs_content_horizontal_padding">40dp</dimen> <dimen name="qs_horizontal_margin">40dp</dimen> <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2, @@ -36,8 +36,8 @@ <dimen name="qs_tiles_page_horizontal_margin">20dp</dimen> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> - <dimen name="qs_media_rec_icon_top_margin">27dp</dimen> - <dimen name="qs_media_rec_album_size">152dp</dimen> + <dimen name="qs_media_rec_icon_top_margin">16dp</dimen> + <dimen name="qs_media_rec_album_size">112dp</dimen> <dimen name="qs_media_rec_album_side_margin">16dp</dimen> <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml index f9ed67d89de7..6a70ebd07ad2 100644 --- a/packages/SystemUI/res/values-sw720dp-land/styles.xml +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -18,10 +18,10 @@ <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:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">120dp</item> <item name="android:paddingVertical">40dp</item> </style> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml index 78d299c483e6..0a46e08da22a 100644 --- a/packages/SystemUI/res/values-sw720dp-port/styles.xml +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -26,10 +26,10 @@ <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:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">240dp</item> <item name="android:paddingVertical">120dp</item> </style> diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml index 07050171470a..927059aa7e40 100644 --- a/packages/SystemUI/res/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp/dimens.xml @@ -22,5 +22,8 @@ <dimen name="controls_padding_horizontal">75dp</dimen> <dimen name="large_screen_shade_header_height">56dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw800dp/dimens.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml new file mode 100644 index 000000000000..0d82217456e4 --- /dev/null +++ b/packages/SystemUI/res/values-sw800dp/dimens.xml @@ -0,0 +1,24 @@ +<?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. + --> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds. --> +<resources> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7d72598c34cf..e8a5e7ed8546 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -437,6 +437,11 @@ This name is in the ComponentName flattened format (package/class) --> <string name="config_screenshotEditor" translatable="false"></string> + <!-- ComponentName for the file browsing app that the system would expect to be used in work + profile. The icon for this app will be shown to the user when informing them that a + screenshot has been saved to work profile. If blank, a default icon will be shown. --> + <string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string> + <!-- Remote copy default activity. Must handle REMOTE_COPY_ACTION intents. This name is in the ComponentName flattened format (package/class) --> <string name="config_remoteCopyPackage" translatable="false"></string> @@ -663,6 +668,16 @@ <item>17</item> <!-- WAKE_REASON_BIOMETRIC --> </integer-array> + <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN) + means systemui will try listening on all postures. + 0 : DEVICE_POSTURE_UNKNOWN + 1 : DEVICE_POSTURE_CLOSED + 2 : DEVICE_POSTURE_HALF_OPENED + 3 : DEVICE_POSTURE_OPENED + 4 : DEVICE_POSTURE_FLIPPED + --> + <integer name="config_face_auth_supported_posture">0</integer> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ecb656091478..890d96444b04 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -334,15 +334,22 @@ <dimen name="overlay_action_chip_spacing">8dp</dimen> <dimen name="overlay_action_chip_text_size">14sp</dimen> <dimen name="overlay_offset_x">16dp</dimen> + <!-- Used for both start and bottom margin of the preview, relative to the action container --> + <dimen name="overlay_preview_container_margin">8dp</dimen> <dimen name="overlay_action_container_margin_horizontal">8dp</dimen> + <dimen name="overlay_action_container_margin_bottom">4dp</dimen> <dimen name="overlay_bg_protection_height">242dp</dimen> <dimen name="overlay_action_container_corner_radius">18dp</dimen> <dimen name="overlay_action_container_padding_vertical">4dp</dimen> <dimen name="overlay_action_container_padding_right">8dp</dimen> + <dimen name="overlay_action_container_padding_end">8dp</dimen> <dimen name="overlay_dismiss_button_tappable_size">48dp</dimen> <dimen name="overlay_dismiss_button_margin">8dp</dimen> + <!-- must be kept aligned with overlay_border_width_neg, below; + overlay_border_width = overlay_border_width_neg * -1 --> <dimen name="overlay_border_width">4dp</dimen> - <!-- need a negative margin for some of the constraints. should be overlay_border_width * -1 --> + <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above; + overlay_border_width_neg = overlay_border_width * -1 --> <dimen name="overlay_border_width_neg">-4dp</dimen> <dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen> @@ -966,6 +973,10 @@ <!-- Biometric Auth Credential values --> <dimen name="biometric_auth_icon_size">48dp</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> + <!-- Starting text size in sp of batteryLevel for wireless charging animation --> <item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen"> 0 @@ -1030,8 +1041,6 @@ <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen> - <!-- Size of the RAT type for CellularTile --> - <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_album_radius">14dp</dimen> @@ -1046,6 +1055,7 @@ <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> <dimen name="qs_media_enabled_seekbar_height">2dp</dimen> <dimen name="qs_media_app_icon_size">24dp</dimen> + <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen> <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen> <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen> @@ -1279,6 +1289,15 @@ <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen> + <!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering --> + <dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen> + + <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering --> + <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen> + + <!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering --> + <dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen> + <!-- The amount of vertical offset for the keyguard during the full shade transition. --> <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen> @@ -1617,6 +1636,8 @@ <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen> + <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen> <!-- Default device corner radius, used for assist UI --> <dimen name="config_rounded_mask_size">0px</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 977adde635aa..066b185c6d1f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -240,7 +240,9 @@ <!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] --> <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string> <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] --> - <string name="screenshot_work_profile_notification" translatable="false">Work screenshots are saved in the work <xliff:g id="app" example="Files">%1$s</xliff:g> app</string> + <string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string> + <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] --> + <string name="screenshot_default_files_app_name">Files</string> <!-- Notification title displayed for screen recording [CHAR LIMIT=50]--> <string name="screenrecord_name">Screen Recorder</string> @@ -1453,10 +1455,10 @@ <string name="notification_conversation_summary_low">No sound or vibration and appears lower in conversation section</string> <!-- [CHAR LIMIT=150] Notification Importance title: normal importance level summary --> - <string name="notification_channel_summary_default">May ring or vibrate based on phone settings</string> + <string name="notification_channel_summary_default">May ring or vibrate based on device settings</string> <!-- [CHAR LIMIT=150] Conversation Notification Importance title: normal conversation level, with bubbling summary --> - <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on phone settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string> + <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on device settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string> <!-- [CHAR LIMIT=150] Notification Importance title: automatic importance level summary --> <string name="notification_channel_summary_automatic">Have the system determine if this notification should make sound or vibration</string> @@ -2357,13 +2359,15 @@ <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] --> <string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string> <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] --> - <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string> + <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string> <!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] --> <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string> - <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] --> + <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] --> <string name="media_transfer_failed">Something went wrong. Try again.</string> <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] --> <string name="media_transfer_loading">Loading</string> + <!-- Default name of the device. [CHAR LIMIT=30] --> + <string name="media_ttt_default_device_type">tablet</string> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <string name="controls_error_timeout">Inactive, check app</string> @@ -2412,6 +2416,8 @@ <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string> <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> + <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title_suggested_device">Suggested Devices</string> <!-- Media Output Broadcast Dialog --> <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] --> @@ -2766,6 +2772,30 @@ <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] --> <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string> + <!-- Title for notification of low stylus battery with percentage. "percentage" is + the value of the battery capacity remaining [CHAR LIMIT=none]--> + <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string> + <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]--> + <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string> + <!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] --> <string name="stylus_battery_low">Stylus battery low</string> + + <!-- Label for a lock screen shortcut to start the camera in video mode. [CHAR_LIMIT=16] --> + <string name="video_camera">Video camera</string> + + <!-- Switch to work profile dialer app for placing a call dialog. --> + <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] --> + <string name="call_from_work_profile_title">Can\'t call from this profile</string> + <!-- Text for switch to work profile for call dialog to guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=NONE] + --> + <string name="call_from_work_profile_text">Your work policy allows you to make phone calls only from the work profile</string> + <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> + <string name="call_from_work_profile_action">Switch to work profile</string> + <!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> + <string name="call_from_work_profile_close">Close</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index b11b6d633f14..9846fc251a27 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -251,11 +251,12 @@ <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> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:paddingHorizontal">32dp</item> + <item name="android:paddingVertical">20dp</item> </style> <style name="AuthCredentialPinPasswordContainerStyle"> diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index 06d425c57577..bf576dc5790b 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright (C) 2021 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,105 +14,73 @@ ~ limitations under the License. --> -<ConstraintSet - xmlns:android="http://schemas.android.com/apk/res/android" +<ConstraintSet xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/large_screen_header_constraint"> - <Constraint - android:id="@+id/clock"> + <Constraint android:id="@+id/clock"> <Layout android:layout_width="wrap_content" android:layout_height="0dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/date" - app:layout_constraintHorizontal_bias="0" - /> - <Transform - android:scaleX="1" - android:scaleY="1" - /> + app:layout_constraintStart_toEndOf="@id/begin_guide" + app:layout_constraintTop_toTopOf="parent" /> + <PropertySet android:alpha="1" /> </Constraint> - <Constraint - android:id="@+id/date"> + <Constraint android:id="@+id/date"> <Layout android:layout_width="wrap_content" android:layout_height="0dp" - app:layout_constraintStart_toEndOf="@id/clock" - app:layout_constraintEnd_toStartOf="@id/carrier_group" - app:layout_constraintTop_toTopOf="parent" + android:layout_marginStart="8dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="0" - /> + app:layout_constraintStart_toEndOf="@id/clock" + app:layout_constraintTop_toTopOf="parent" /> + <PropertySet android:alpha="1" /> </Constraint> - <Constraint - android:id="@+id/carrier_group"> + <Constraint android:id="@+id/carrier_group"> <Layout - app:layout_constraintWidth_min="48dp" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constrainedWidth="true" android:layout_gravity="end|center_vertical" - android:layout_marginStart="8dp" - app:layout_constraintStart_toEndOf="@id/date" - app:layout_constraintEnd_toStartOf="@id/statusIcons" - app:layout_constraintTop_toTopOf="@id/clock" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" - /> - <PropertySet - android:alpha="1" - /> + app:layout_constraintEnd_toStartOf="@id/statusIcons" + app:layout_constraintStart_toEndOf="@id/date" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="wrap" + app:layout_constraintWidth_min="48dp" /> + <PropertySet android:alpha="1" /> </Constraint> - <Constraint - android:id="@+id/statusIcons"> + <Constraint android:id="@+id/statusIcons"> <Layout - app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" android:layout_width="wrap_content" android:layout_height="@dimen/large_screen_shade_header_min_height" - app:layout_constraintStart_toEndOf="@id/carrier_group" - app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" - app:layout_constraintTop_toTopOf="@id/clock" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" - /> - <PropertySet - android:alpha="1" - /> + app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="@id/carrier_group"/> + <PropertySet android:alpha="1" /> </Constraint> - <Constraint - android:id="@+id/batteryRemainingIcon"> + <Constraint android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" - app:layout_constraintStart_toEndOf="@id/statusIcons" - app:layout_constraintEnd_toStartOf="@id/privacy_container" - app:layout_constraintTop_toTopOf="@id/clock" app:layout_constraintBottom_toBottomOf="parent" - /> - <PropertySet - android:alpha="1" - /> + app:layout_constraintEnd_toStartOf="@id/privacy_container" + app:layout_constraintTop_toTopOf="parent" /> + <PropertySet android:alpha="1" /> </Constraint> - <Constraint - android:id="@+id/privacy_container"> + <Constraint android:id="@+id/privacy_container"> <Layout android:layout_width="wrap_content" android:layout_height="@dimen/large_screen_shade_header_min_height" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="@id/date" - app:layout_constraintStart_toEndOf="@id/batteryRemainingIcon" - app:layout_constraintHorizontal_bias="1" - /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/end_guide" + app:layout_constraintTop_toTopOf="parent" /> </Constraint> - </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 1eb621e0368b..d9c81af54a12 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -66,6 +66,21 @@ app:layout_constraintTop_toBottomOf="@id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed" /> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -75,9 +90,8 @@ app:layout_constraintEnd_toStartOf="@id/action_button_guideline" app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" - app:layout_constraintStart_toStartOf="@id/header_title" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index 7de0a5e0e8c4..0cdc0f9505bc 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -58,6 +58,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed"/> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -67,10 +82,9 @@ android:layout_marginTop="0dp" app:layout_constrainedWidth="true" app:layout_constraintEnd_toStartOf="@id/actionPlayPause" - app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml index eca2b2acb079..d97031f35d6b 100644 --- a/packages/SystemUI/res/xml/qs_header.xml +++ b/packages/SystemUI/res/xml/qs_header.xml @@ -56,13 +56,9 @@ <Layout android:layout_width="wrap_content" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constrainedWidth="true" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/carrier_group" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintHorizontal_chainStyle="spread_inside" /> </Constraint> @@ -87,39 +83,27 @@ <Constraint android:id="@+id/statusIcons"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/space" + app:layout_constraintWidth_default="wrap" + app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> <Constraint android:id="@+id/batteryRemainingIcon"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintWidth_default="wrap" app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" - app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> - - <Constraint - android:id="@id/space"> - <Layout - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintStart_toEndOf="@id/date" - app:layout_constraintEnd_toStartOf="@id/statusIcons" - /> - </Constraint> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt new file mode 100644 index 000000000000..a9a5cf974f95 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.hardware + +import android.view.InputDevice + +/** + * Returns true if [InputDevice] is electronic components to allow a user to use an active stylus in + * the host device or a passive stylus is detected by the host device. + */ +val InputDevice.isInternalStylusSource: Boolean + get() = isAnyStylusSource && !isExternal + +/** Returns true if [InputDevice] is an active stylus. */ +val InputDevice.isExternalStylusSource: Boolean + get() = isAnyStylusSource && isExternal + +/** + * Returns true if [InputDevice] supports any stylus source. + * + * @see InputDevice.isInternalStylusSource + * @see InputDevice.isExternalStylusSource + */ +val InputDevice.isAnyStylusSource: Boolean + get() = supportsSource(InputDevice.SOURCE_STYLUS) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt new file mode 100644 index 000000000000..f020b4ec4983 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.hardware + +import android.hardware.input.InputManager +import android.view.InputDevice + +/** + * Gets information about all input devices in the system and returns as a lazy [Sequence]. + * + * For performance reasons, it is preferred to operate atop the returned [Sequence] to ensure each + * operation is executed on an element-per-element basis yet customizable. + * + * For example: + * ```kotlin + * val stylusDevices = inputManager.getInputDeviceSequence().filter { + * it.supportsSource(InputDevice.SOURCE_STYLUS) + * } + * + * val hasInternalStylus = stylusDevices.any { it.isInternal } + * val hasExternalStylus = stylusDevices.any { !it.isInternal } + * ``` + * + * @return a [Sequence] of [InputDevice]. + */ +fun InputManager.getInputDeviceSequence(): Sequence<InputDevice> = + inputDeviceIds.asSequence().mapNotNull { getInputDevice(it) } + +/** + * Returns the first [InputDevice] matching the given predicate, or null if no such [InputDevice] + * was found. + */ +fun InputManager.findInputDevice(predicate: (InputDevice) -> Boolean): InputDevice? = + getInputDeviceSequence().find { predicate(it) } + +/** + * Returns true if [any] [InputDevice] matches with [predicate]. + * + * For example: + * ```kotlin + * val hasStylusSupport = inputManager.hasInputDevice { it.isStylusSupport() } + * val hasStylusPen = inputManager.hasInputDevice { it.isStylusPen() } + * ``` + */ +fun InputManager.hasInputDevice(predicate: (InputDevice) -> Boolean): Boolean = + getInputDeviceSequence().any { predicate(it) } + +/** Returns true if host device has any [InputDevice] where [InputDevice.isInternalStylusSource]. */ +fun InputManager.hasInternalStylusSource(): Boolean = hasInputDevice { it.isInternalStylusSource } + +/** Returns true if host device has any [InputDevice] where [InputDevice.isExternalStylusSource]. */ +fun InputManager.hasExternalStylusSource(): Boolean = hasInputDevice { it.isExternalStylusSource } + +/** Returns true if host device has any [InputDevice] where [InputDevice.isAnyStylusSource]. */ +fun InputManager.hasAnyStylusSource(): Boolean = hasInputDevice { it.isAnyStylusSource } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt index 25d272185bc0..9b73cc3ea9f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -48,48 +48,28 @@ constructor( val drawableInsetSize: Int try { val keyShadowBlur = - attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f) val keyShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f) val keyShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f) val keyShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f) mKeyShadowInfo = - ShadowInfo( - keyShadowBlur.toFloat(), - keyShadowOffsetX.toFloat(), - keyShadowOffsetY.toFloat(), - keyShadowAlpha - ) + ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha) val ambientShadowBlur = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowBlur, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f) val ambientShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f) val ambientShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f) val ambientShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f) mAmbientShadowInfo = ShadowInfo( - ambientShadowBlur.toFloat(), - ambientShadowOffsetX.toFloat(), - ambientShadowOffsetY.toFloat(), + ambientShadowBlur, + ambientShadowOffsetX, + ambientShadowOffsetY, ambientShadowAlpha ) drawableSize = diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index fd41cb0630dd..6bfaf5e49820 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -283,17 +283,6 @@ public class ActivityManagerWrapper { } /** - * @return whether screen pinning is active. - */ - public boolean isScreenPinningActive() { - try { - return getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED; - } catch (RemoteException e) { - return false; - } - } - - /** * @return whether screen pinning is enabled. */ public boolean isScreenPinningEnabled() { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index 8af934f66b2a..dd52cfbdc80f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -16,6 +16,7 @@ package com.android.systemui.shared.system; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.app.TaskStackListener; @@ -27,6 +28,8 @@ import android.os.Trace; import android.util.Log; import android.window.TaskSnapshot; +import androidx.annotation.VisibleForTesting; + import com.android.internal.os.SomeArgs; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -43,15 +46,51 @@ public class TaskStackChangeListeners { private final Impl mImpl; + /** + * Proxies calls to the given handler callback synchronously for testing purposes. + */ + private static class TestSyncHandler extends Handler { + private Handler.Callback mCb; + + public TestSyncHandler() { + super(Looper.getMainLooper()); + } + + public void setCallback(Handler.Callback cb) { + mCb = cb; + } + + @Override + public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { + return mCb.handleMessage(msg); + } + } + private TaskStackChangeListeners() { mImpl = new Impl(Looper.getMainLooper()); } + private TaskStackChangeListeners(Handler h) { + mImpl = new Impl(h); + } + public static TaskStackChangeListeners getInstance() { return INSTANCE; } /** + * Returns an instance of the listeners that can be called upon synchronously for testsing + * purposes. + */ + @VisibleForTesting + public static TaskStackChangeListeners getTestInstance() { + TestSyncHandler h = new TestSyncHandler(); + TaskStackChangeListeners l = new TaskStackChangeListeners(h); + h.setCallback(l.mImpl); + return l; + } + + /** * Registers a task stack listener with the system. * This should be called on the main thread. */ @@ -71,7 +110,15 @@ public class TaskStackChangeListeners { } } - private static class Impl extends TaskStackListener implements Handler.Callback { + /** + * Returns an instance of the listener to call upon from tests. + */ + @VisibleForTesting + public TaskStackListener getListenerImpl() { + return mImpl; + } + + private class Impl extends TaskStackListener implements Handler.Callback { private static final int ON_TASK_STACK_CHANGED = 1; private static final int ON_TASK_SNAPSHOT_CHANGED = 2; @@ -104,10 +151,14 @@ public class TaskStackChangeListeners { private final Handler mHandler; private boolean mRegistered; - Impl(Looper looper) { + private Impl(Looper looper) { mHandler = new Handler(looper, this); } + private Impl(Handler handler) { + mHandler = handler; + } + public void addListener(TaskStackChangeListener listener) { synchronized (mTaskStackListeners) { mTaskStackListeners.add(listener); diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt index 7f2933e44b32..c9e57b45612c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt @@ -15,21 +15,51 @@ package com.android.systemui.unfold.system import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration +import android.os.Trace +import com.android.systemui.shared.system.TaskStackChangeListener +import com.android.systemui.shared.system.TaskStackChangeListeners import com.android.systemui.unfold.util.CurrentActivityTypeProvider import javax.inject.Inject import javax.inject.Singleton @Singleton -class ActivityManagerActivityTypeProvider @Inject constructor( - private val activityManager: ActivityManager -) : CurrentActivityTypeProvider { +class ActivityManagerActivityTypeProvider +@Inject +constructor(private val activityManager: ActivityManager) : CurrentActivityTypeProvider { override val isHomeActivity: Boolean? - get() { - val activityType = activityManager.getRunningTasks(/* maxNum= */ 1) - ?.getOrNull(0)?.topActivityType ?: return null + get() = _isHomeActivity - return activityType == WindowConfiguration.ACTIVITY_TYPE_HOME + private var _isHomeActivity: Boolean? = null + + + override fun init() { + _isHomeActivity = activityManager.isOnHomeActivity() + TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener) + } + + override fun uninit() { + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener) + } + + private val taskStackChangeListener = + object : TaskStackChangeListener { + override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) { + _isHomeActivity = taskInfo.isHomeActivity() + } + } + + private fun RunningTaskInfo.isHomeActivity(): Boolean = + topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME + + private fun ActivityManager.isOnHomeActivity(): Boolean? { + try { + Trace.beginSection("isOnHomeActivity") + return getRunningTasks(/* maxNum= */ 1)?.firstOrNull()?.isHomeActivity() + } finally { + Trace.endSection() } + } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt index 24ae42ae4db2..fe607e16661c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt @@ -19,7 +19,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig import com.android.systemui.unfold.config.UnfoldTransitionConfig -import com.android.systemui.unfold.dagger.UnfoldBackground +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.util.CurrentActivityTypeProvider @@ -56,6 +56,6 @@ abstract class SystemUnfoldSharedModule { abstract fun mainHandler(@Main handler: Handler): Handler @Binds - @UnfoldBackground + @UnfoldSingleThreadBg abstract fun backgroundExecutor(@UiBackground executor: Executor): Executor } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8f38e5800015..a45ce422dca5 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -38,9 +38,11 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.log.dagger.KeyguardClockLog +import com.android.systemui.log.dagger.KeyguardSmallClockLog +import com.android.systemui.log.dagger.KeyguardLargeClockLog import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -73,16 +75,18 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - @KeyguardClockLog private val logBuffer: LogBuffer?, + @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?, + @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?, private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { field = value if (value != null) { - if (logBuffer != null) { - value.setLogBuffer(logBuffer) - } + smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.smallClock.logBuffer = smallLogBuffer + largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.largeClock.logBuffer = largeLogBuffer value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) @@ -325,4 +329,8 @@ open class ClockEventController @Inject constructor( } } } + + companion object { + private val TAG = ClockEventController::class.simpleName!! + } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 5bb9367fa4a5..e0cf7b6a2bc4 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -50,6 +50,7 @@ import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_RESET import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED +import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE @@ -126,6 +127,7 @@ private object InternalFaceAuthReasons { const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed" const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED = "Face auth stopped because non strong biometric allowed changed" + const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed." } /** @@ -173,6 +175,7 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : return PowerManager.wakeReasonToString(extraInfo) } }, + @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED), @Deprecated( "Not a face auth trigger.", ReplaceWith( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 62babadc45d8..4acbb0aaf1d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -7,7 +7,6 @@ import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -20,11 +19,15 @@ import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import kotlin.Unit; + /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -87,6 +90,7 @@ public class KeyguardClockSwitch extends RelativeLayout { private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; @VisibleForTesting boolean mAnimateOnLayout = true; + private LogBuffer mLogBuffer = null; public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -113,6 +117,14 @@ public class KeyguardClockSwitch extends RelativeLayout { onDensityOrFontScaleChanged(); } + public void setLogBuffer(LogBuffer logBuffer) { + mLogBuffer = logBuffer; + } + + public LogBuffer getLogBuffer() { + return mLogBuffer; + } + void setClock(ClockController clock, int statusBarState) { mClock = clock; @@ -121,12 +133,16 @@ public class KeyguardClockSwitch extends RelativeLayout { mLargeClockFrame.removeAllViews(); if (clock == null) { - Log.e(TAG, "No clock being shown"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); + } return; } // Attach small and big clock views to hierarchy. - Log.i(TAG, "Attached new clock views to switch"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); + } mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); updateClockTargetRegions(); @@ -152,8 +168,18 @@ public class KeyguardClockSwitch extends RelativeLayout { } private void updateClockViews(boolean useLargeClock, boolean animate) { - Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate - + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { + msg.setBool1(useLargeClock); + msg.setBool2(animate); + msg.setBool3(mChildrenAreLaidOut); + return Unit.INSTANCE; + }, (msg) -> "updateClockViews" + + "; useLargeClock=" + msg.getBool1() + + "; animate=" + msg.getBool2() + + "; mChildrenAreLaidOut=" + msg.getBool3()); + } + if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); @@ -183,6 +209,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (!animate) { out.setAlpha(0f); + out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setVisibility(VISIBLE); mStatusArea.setTranslationY(statusAreaYTranslation); @@ -198,7 +225,10 @@ public class KeyguardClockSwitch extends RelativeLayout { direction * -mClockSwitchYAmount)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockOutAnim = null; + if (mClockOutAnim == animation) { + out.setVisibility(INVISIBLE); + mClockOutAnim = null; + } } }); @@ -212,7 +242,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockInAnim = null; + if (mClockInAnim == animation) { + mClockInAnim = null; + } } }); @@ -225,7 +257,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mStatusAreaAnim = null; + if (mStatusAreaAnim == animation) { + mStatusAreaAnim = null; + } } }); mStatusAreaAnim.start(); @@ -269,7 +303,9 @@ public class KeyguardClockSwitch extends RelativeLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame: " + mSmallClockFrame); + pw.println(" mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha()); pw.println(" mLargeClockFrame: " + mLargeClockFrame); + pw.println(" mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha()); pw.println(" mStatusArea: " + mStatusArea); pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 788f1200d603..88ce2a74c99d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -38,8 +38,11 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.log.dagger.KeyguardClockLog; import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; @@ -62,6 +65,8 @@ import javax.inject.Inject; */ public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch> implements Dumpable { + private static final String TAG = "KeyguardClockSwitchController"; + private final StatusBarStateController mStatusBarStateController; private final ClockRegistry mClockRegistry; private final KeyguardSliceViewController mKeyguardSliceViewController; @@ -70,6 +75,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final SecureSettings mSecureSettings; private final DumpManager mDumpManager; private final ClockEventController mClockEventController; + private final LogBuffer mLogBuffer; private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @@ -119,7 +125,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController) { + ClockEventController clockEventController, + @KeyguardClockLog LogBuffer logBuffer) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -131,6 +138,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mDumpManager = dumpManager; mClockEventController = clockEventController; + mLogBuffer = logBuffer; + mView.setLogBuffer(mLogBuffer); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); @@ -337,10 +346,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2; } else { - // This is only called if we've never shown the large clock as the frame is inflated - // with 'gone', but then the visibility is never set when it is animated away by - // KeyguardClockSwitch, instead it is removed from the view hierarchy. - // TODO(b/261755021): Cleanup Large Frame Visibility int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } @@ -358,15 +363,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mLargeClockFrame.getVisibility() == View.VISIBLE) { return clock.getLargeClock().getView().getHeight(); } else { - // Is not called except in certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return clock.getSmallClock().getView().getHeight(); } } boolean isClockTopAligned() { - // Returns false except certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return mLargeClockFrame.getVisibility() != View.VISIBLE; } @@ -378,6 +379,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setClock(ClockController clock) { + if (clock != null && mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "New Clock"); + } + mClockEventController.setClock(clock); mView.setClock(clock, mStatusBarStateController.getState()); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index deead1959b8a..1a06b5f1c767 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -39,6 +39,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, + val postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index d4ca8e34fb32..ea84438bf4ba 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -29,6 +29,9 @@ import android.view.View; import android.view.View.OnKeyListener; import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import android.window.OnBackAnimationCallback; + +import androidx.annotation.NonNull; import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -394,6 +397,14 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> } /** + * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture. + */ + @NonNull + public OnBackAnimationCallback getBackCallback() { + return mKeyguardSecurityContainerController.getBackCallback(); + } + + /** * Allows the media keys to work when the keyguard is showing. * The media keys should be of no interest to the actual keyguard view(s), * so intercepting them here should not be of any harm. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 5d7a6f122e69..e4f85db3971e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -32,6 +32,7 @@ import static androidx.constraintlayout.widget.ConstraintSet.START; import static androidx.constraintlayout.widget.ConstraintSet.TOP; import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; +import static com.android.systemui.animation.InterpolatorsAndroidX.DECELERATE_QUINT; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; import static java.lang.Integer.max; @@ -73,6 +74,8 @@ import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -135,7 +138,9 @@ public class KeyguardSecurityContainer extends ConstraintLayout { private static final float MIN_DRAG_SIZE = 10; // How much to scale the default slop by, to avoid accidental drags. private static final float SLOP_SCALE = 4f; - + @VisibleForTesting + // How much the view scales down to during back gestures. + static final float MIN_BACK_SCALE = 0.9f; @VisibleForTesting KeyguardSecurityViewFlipper mSecurityViewFlipper; private GlobalSettings mGlobalSettings; @@ -240,6 +245,33 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } }; + private final OnBackAnimationCallback mBackCallback = new OnBackAnimationCallback() { + @Override + public void onBackCancelled() { + // TODO(b/259608500): Remove once back API auto animates progress to 0 on cancel. + resetScale(); + } + + @Override + public void onBackInvoked() { } + + @Override + public void onBackProgressed(BackEvent event) { + float progress = event.getProgress(); + // TODO(b/263819310): Update the interpolator to match spec. + float scale = MIN_BACK_SCALE + + (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress)); + setScale(scale); + } + }; + /** + * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture. + */ + @NonNull + OnBackAnimationCallback getBackCallback() { + return mBackCallback; + } + // Used to notify the container when something interesting happens. public interface SecurityCallback { /** @@ -736,6 +768,15 @@ public class KeyguardSecurityContainer extends ConstraintLayout { mViewMode.onDensityOrFontScaleChanged(); } + void resetScale() { + setScale(1); + } + + private void setScale(float scale) { + setScaleX(scale); + setScaleY(scale); + } + /** * Enscapsulates the differences between bouncer modes for the container. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index a72a484fb6f1..57bfe5421049 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -40,7 +40,9 @@ import android.util.Log; import android.util.Slog; import android.view.MotionEvent; import android.view.View; +import android.window.OnBackAnimationCallback; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -479,6 +481,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard /** Called when the bouncer changes visibility. */ public void onBouncerVisibilityChanged(@View.Visibility int visibility) { setBouncerVisible(visibility == View.VISIBLE); + if (visibility == View.INVISIBLE) { + mView.resetScale(); + } } private void setBouncerVisible(boolean visible) { @@ -588,6 +593,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } /** + * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture. + */ + @NonNull + OnBackAnimationCallback getBackCallback() { + return mView.getBackCallback(); + } + + /** * Switches to the given security view unless it's already being shown, in which case * this is a no-op. * diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index aec30632c41e..b53b868025e8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.util.Slog; import com.android.keyguard.KeyguardClockSwitch.ClockSize; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ClockAnimations; @@ -62,14 +63,16 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV ConfigurationController configurationController, DozeParameters dozeParameters, FeatureFlags featureFlags, - ScreenOffAnimationController screenOffAnimationController) { + ScreenOffAnimationController screenOffAnimationController, + KeyguardLogger logger) { super(keyguardStatusView); mKeyguardSliceViewController = keyguardSliceViewController; mKeyguardClockSwitchController = keyguardClockSwitchController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mConfigurationController = configurationController; mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, - dozeParameters, screenOffAnimationController, /* animateYPos= */ true); + dozeParameters, screenOffAnimationController, /* animateYPos= */ true, + logger.getBuffer()); mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled( featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION)); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 93027c1914ee..a9695dd62ba4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -63,11 +63,13 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_RE import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT; +import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import android.annotation.AnyThread; import android.annotation.MainThread; @@ -84,7 +86,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; @@ -108,7 +109,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; @@ -141,6 +141,7 @@ import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -154,6 +155,7 @@ import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -170,6 +172,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.Executor; @@ -269,21 +272,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName( "com.android.settings", "com.android.settings.FallbackHome"); - /** - * If true, the system is in the half-boot-to-decryption-screen state. - * Prudently disable lockscreen. - */ - public static final boolean CORE_APPS_ONLY; - - static { - try { - CORE_APPS_ONLY = IPackageManager.Stub.asInterface( - ServiceManager.getService("package")).isOnlyCoreApps(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - private final Context mContext; private final UserTracker mUserTracker; private final KeyguardUpdateMonitorLogger mLogger; @@ -345,18 +333,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; - private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver; private final ContentObserver mTimeFormatChangeObserver; private boolean mSwitchingUser; private boolean mDeviceInteractive; - private boolean mSfpsRequireScreenOnToAuthPrefEnabled; private final SubscriptionManager mSubscriptionManager; private final TelephonyListenerManager mTelephonyListenerManager; private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; + private final DevicePostureController mPostureController; private final BroadcastDispatcher mBroadcastDispatcher; private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; @@ -374,6 +361,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; private final boolean mWakeOnFingerprintAcquiredStart; + @VisibleForTesting + @DevicePostureController.DevicePostureInt + protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; private List<SubscriptionInfo> mSubscriptionInfo; @@ -384,6 +374,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mLogoutEnabled; private boolean mIsFaceEnrolled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private int mPostureState = DEVICE_POSTURE_UNKNOWN; + private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; /** * Short delay before restarting fingerprint authentication after a successful try. This should @@ -711,8 +703,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void setKeyguardGoingAway(boolean goingAway) { mKeyguardGoingAway = goingAway; - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + if (mKeyguardGoingAway) { + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + } + + /** + * Whether keyguard is going away due to screen off or device entry. + */ + public boolean isKeyguardGoingAway() { + return mKeyguardGoingAway; } /** @@ -1784,6 +1786,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; @VisibleForTesting + final DevicePostureController.Callback mPostureCallback = + new DevicePostureController.Callback() { + @Override + public void onPostureChanged(int posture) { + mPostureState = posture; + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } + }; + + @VisibleForTesting CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; @@ -1943,9 +1956,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onFinishedGoingToSleep(arg1); } } - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, + updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP); + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } private void handleScreenTurnedOff() { @@ -2048,7 +2061,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager, @Nullable BiometricManager biometricManager, - FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) { + FaceWakeUpTriggersConfig faceWakeUpTriggersConfig, + DevicePostureController devicePostureController, + Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2077,6 +2092,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mDreamManager = dreamManager; mTelephonyManager = telephonyManager; mDevicePolicyManager = devicePolicyManager; + mPostureController = devicePostureController; mPackageManager = packageManager; mFpm = fingerprintManager; mFaceManager = faceManager; @@ -2088,6 +2104,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger( + R.integer.config_face_auth_supported_posture); mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @@ -2278,6 +2296,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED)); } }); + if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) { + mPostureController.addCallback(mPostureCallback); + } updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT); TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); @@ -2311,30 +2332,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Settings.System.getUriFor(Settings.System.TIME_12_24), false, mTimeFormatChangeObserver, UserHandle.USER_ALL); - updateSfpsRequireScreenOnToAuthPref(); - mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - updateSfpsRequireScreenOnToAuthPref(); - } - }; - - mContext.getContentResolver().registerContentObserver( - mSecureSettings.getUriFor( - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED), - false, - mSfpsRequireScreenOnToAuthPrefObserver, - getCurrentUser()); - } - - protected void updateSfpsRequireScreenOnToAuthPref() { - final int defaultSfpsRequireScreenOnToAuthValue = - mContext.getResources().getBoolean( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0; - mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser( - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, - defaultSfpsRequireScreenOnToAuthValue, - getCurrentUser()) != 0; + mFingerprintInteractiveToAuthProvider = interactiveToAuthProvider.orElse(null); } private void initializeSimState() { @@ -2729,8 +2727,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean shouldListenSideFpsState = true; if (isSideFps) { + final boolean interactiveToAuthEnabled = + mFingerprintInteractiveToAuthProvider != null && + mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser()); shouldListenSideFpsState = - mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true; + interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true; } boolean shouldListen = shouldListenKeyguardState && shouldListenUserState @@ -2742,7 +2743,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab user, shouldListen, biometricEnabledForUser, - mPrimaryBouncerIsOrWillBeShowing, + mPrimaryBouncerIsOrWillBeShowing, userCanSkipBouncer, mCredentialAttempted, mDeviceInteractive, @@ -2802,6 +2803,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); + final boolean isPostureAllowedForFaceAuth = + mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true + : (mPostureState == mConfigFaceAuthSupportedPosture); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2818,7 +2822,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && faceAndFpNotAuthenticated - && !mGoingToSleep; + && !mGoingToSleep + && isPostureAllowedForFaceAuth; // Aggregate relevant fields for debug logging. logListenerModelData( @@ -2838,6 +2843,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardGoingAway, shouldListenForFaceAssistant, mOccludingAppRequestingFace, + isPostureAllowedForFaceAuth, mIsPrimaryUser, mSecureCameraLaunched, supportsDetect, @@ -2923,7 +2929,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab getKeyguardSessionId(), faceAuthUiEvent.getExtraInfo() ); - + mLogger.logFaceUnlockPossible(unlockPossible); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); @@ -3845,11 +3851,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver); } - if (mSfpsRequireScreenOnToAuthPrefObserver != null) { - mContext.getContentResolver().unregisterContentObserver( - mSfpsRequireScreenOnToAuthPrefObserver); - } - try { ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver); } catch (RemoteException e) { @@ -3926,8 +3927,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } else if (isSfpsSupported()) { pw.println(" sfpsEnrolled=" + isSfpsEnrolled()); pw.println(" shouldListenForSfps=" + shouldListenForFingerprint(false)); - pw.println(" mSfpsRequireScreenOnToAuthPrefEnabled=" - + mSfpsRequireScreenOnToAuthPrefEnabled); + if (isSfpsEnrolled()) { + final boolean interactiveToAuthEnabled = + mFingerprintInteractiveToAuthProvider != null && + mFingerprintInteractiveToAuthProvider + .isEnabled(getCurrentUser()); + pw.println(" interactiveToAuthEnabled=" + + interactiveToAuthEnabled); + } } new DumpsysTableLogger( "KeyguardFingerprintListen", diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java index bde06929a3d1..7e48193bfc62 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java @@ -22,6 +22,8 @@ import android.view.View; import android.view.ViewPropertyAnimator; import com.android.systemui.animation.Interpolators; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; @@ -31,11 +33,14 @@ import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.google.errorprone.annotations.CompileTimeConstant; + /** * Helper class for updating visibility of keyguard views based on keyguard and status bar state. * This logic is shared by both the keyguard status view and the keyguard user switcher. */ public class KeyguardVisibilityHelper { + private static final String TAG = "KeyguardVisibilityHelper"; private View mView; private final KeyguardStateController mKeyguardStateController; @@ -46,17 +51,26 @@ public class KeyguardVisibilityHelper { private boolean mLastOccludedState = false; private boolean mIsUnoccludeTransitionFlagEnabled = false; private final AnimationProperties mAnimationProperties = new AnimationProperties(); + private final LogBuffer mLogBuffer; public KeyguardVisibilityHelper(View view, KeyguardStateController keyguardStateController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, - boolean animateYPos) { + boolean animateYPos, + LogBuffer logBuffer) { mView = view; mKeyguardStateController = keyguardStateController; mDozeParameters = dozeParameters; mScreenOffAnimationController = screenOffAnimationController; mAnimateYPos = animateYPos; + mLogBuffer = logBuffer; + } + + private void log(@CompileTimeConstant String message) { + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.DEBUG, message); + } } public boolean isVisibilityAnimating() { @@ -94,6 +108,9 @@ public class KeyguardVisibilityHelper { .setStartDelay(mKeyguardStateController.getKeyguardFadingAwayDelay()) .setDuration(mKeyguardStateController.getShortenedFadingAwayDuration()) .start(); + log("goingToFullShade && keyguardFadingAway"); + } else { + log("goingToFullShade && !keyguardFadingAway"); } } else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) { mView.setVisibility(View.VISIBLE); @@ -105,6 +122,7 @@ public class KeyguardVisibilityHelper { .setDuration(320) .setInterpolator(Interpolators.ALPHA_IN) .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); + log("keyguardFadingAway transition w/ Y Aniamtion"); } else if (statusBarState == KEYGUARD) { if (keyguardFadingAway) { mKeyguardViewVisibilityAnimating = true; @@ -125,9 +143,13 @@ public class KeyguardVisibilityHelper { true /* animate */); animator.setDuration(duration) .setStartDelay(delay); + log("keyguardFadingAway transition w/ Y Aniamtion"); + } else { + log("keyguardFadingAway transition w/o Y Animation"); } animator.start(); } else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) { + log("ScreenOff transition"); mKeyguardViewVisibilityAnimating = true; // Ask the screen off animation controller to animate the keyguard visibility for us @@ -136,6 +158,7 @@ public class KeyguardVisibilityHelper { mView, mAnimateKeyguardStatusViewVisibleEndRunnable); } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) { // An activity was displayed over the lock screen, and has now gone away + log("Unoccluded transition"); mView.setVisibility(View.VISIBLE); mView.setAlpha(0f); @@ -146,12 +169,14 @@ public class KeyguardVisibilityHelper { .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable) .start(); } else { + log("Direct set Visibility to VISIBLE"); mView.setVisibility(View.VISIBLE); if (!mIsUnoccludeTransitionFlagEnabled) { mView.setAlpha(1f); } } } else { + log("Direct set Visibility to GONE"); mView.setVisibility(View.GONE); mView.setAlpha(1f); } @@ -162,14 +187,18 @@ public class KeyguardVisibilityHelper { private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = () -> { mKeyguardViewVisibilityAnimating = false; mView.setVisibility(View.INVISIBLE); + log("Callback Set Visibility to INVISIBLE"); }; private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = () -> { mKeyguardViewVisibilityAnimating = false; mView.setVisibility(View.GONE); + log("CallbackSet Visibility to GONE"); }; private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> { mKeyguardViewVisibilityAnimating = false; + mView.setVisibility(View.VISIBLE); + log("Callback Set Visibility to VISIBLE"); }; } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index b84fb08d53a8..2c7eceba48a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -17,36 +17,46 @@ package com.android.keyguard.logging import com.android.systemui.log.dagger.KeyguardLog -import com.android.systemui.plugins.log.ConstantStringsLogger -import com.android.systemui.plugins.log.ConstantStringsLoggerImpl import com.android.systemui.plugins.log.LogBuffer -import com.android.systemui.plugins.log.LogLevel.DEBUG -import com.android.systemui.plugins.log.LogLevel.ERROR -import com.android.systemui.plugins.log.LogLevel.INFO -import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject -private const val TAG = "KeyguardLog" +private const val BIO_TAG = "KeyguardLog" /** * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be * an overkill. */ -class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) : - ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { - - fun logException(ex: Exception, @CompileTimeConstant logMsg: String) { - buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex) - } - - fun v(msg: String, arg: Any) { - buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" }) - } +class KeyguardLogger +@Inject +constructor( + @KeyguardLog val buffer: LogBuffer, +) { + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + ex: Throwable? = null, + ) = buffer.log(tag, level, msg, ex) - fun i(msg: String, arg: Any) { - buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" }) + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + arg: Any, + ) { + buffer.log( + tag, + level, + { + str1 = msg + str2 = arg.toString() + }, + { "$str1: $str2" } + ) } @JvmOverloads @@ -56,8 +66,8 @@ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuf msg: String? = null ) { buffer.log( - TAG, - DEBUG, + BIO_TAG, + LogLevel.DEBUG, { str1 = context str2 = "$msgId" diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 21d3b24174b6..5b4245595be9 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -132,6 +132,12 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" }) } + fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) { + logBuffer.log(TAG, DEBUG, + { bool1 = isFaceUnlockPossible }, + {"isUnlockWithFacePossible: $bool1"}) + } + fun logFingerprintAuthForWrongUser(authUserId: Int) { logBuffer.log(TAG, DEBUG, { int1 = authUserId }, diff --git a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt new file mode 100644 index 000000000000..249b3fe97d81 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard.logging + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.TrustModel +import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import javax.inject.Inject + +/** Logging helper for trust repository. */ +@SysUISingleton +class TrustRepositoryLogger +@Inject +constructor( + @KeyguardUpdateMonitorLog private val logBuffer: LogBuffer, +) { + fun onTrustChanged( + enabled: Boolean, + newlyUnlocked: Boolean, + userId: Int, + flags: Int, + trustGrantedMessages: List<String>? + ) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = enabled + bool2 = newlyUnlocked + int1 = userId + int2 = flags + str1 = trustGrantedMessages?.joinToString() + }, + { + "onTrustChanged enabled: $bool1, newlyUnlocked: $bool2, " + + "userId: $int1, flags: $int2, grantMessages: $str1" + } + ) + } + + fun trustListenerRegistered() { + logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#registerTrustListener") + } + + fun trustListenerUnregistered() { + logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#unregisterTrustListener") + } + + fun trustModelEmitted(value: TrustModel) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = value.userId + bool1 = value.isTrusted + }, + { "trustModel emitted: userId: $int1 isTrusted: $bool1" } + ) + } + + fun isCurrentUserTrusted(isCurrentUserTrusted: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = isCurrentUserTrusted }, + { "isCurrentUserTrusted emitted: $bool1" } + ) + } + + companion object { + const val TAG = "TrustRepositoryLog" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 0fc9ef96f6e9..632fcdc16259 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -22,8 +22,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; -import androidx.annotation.Nullable; - import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; @@ -55,7 +53,6 @@ public abstract class SystemUIInitializer { mContext = context; } - @Nullable protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder(); /** @@ -72,11 +69,6 @@ public abstract class SystemUIInitializer { * Starts the initialization process. This stands up the Dagger graph. */ public void init(boolean fromTest) throws ExecutionException, InterruptedException { - GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder(); - if (globalBuilder == null) { - return; - } - mRootComponent = getGlobalRootComponentBuilder() .context(mContext) .instrumentationTest(fromTest) @@ -127,7 +119,6 @@ public abstract class SystemUIInitializer { .setBackAnimation(Optional.ofNullable(null)) .setDesktopMode(Optional.ofNullable(null)); } - mSysUIComponent = builder.build(); if (initializeComponents) { mSysUIComponent.init(); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt index 55c095b0be25..8aa3040c6015 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui -import android.app.Application import android.content.Context import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent import com.android.systemui.dagger.GlobalRootComponent @@ -25,17 +24,7 @@ import com.android.systemui.dagger.GlobalRootComponent * {@link SystemUIInitializer} that stands up AOSP SystemUI. */ class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) { - - override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? { - return when (Application.getProcessName()) { - SCREENSHOT_CROSS_PROFILE_PROCESS -> null - else -> DaggerReferenceGlobalRootComponent.builder() - } - } - - companion object { - private const val SYSTEMUI_PROCESS = "com.android.systemui" - private const val SCREENSHOT_CROSS_PROFILE_PROCESS = - "$SYSTEMUI_PROCESS:screenshot_cross_profile" + override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder { + return DaggerReferenceGlobalRootComponent.builder() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt index e4c197ff940e..1404053e4618 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt @@ -35,13 +35,13 @@ class AuthBiometricFingerprintAndFaceIconController( override val actsAsConfirmButton: Boolean = true - override fun shouldAnimateForTransition( + override fun shouldAnimateIconViewForTransition( @BiometricState oldState: Int, @BiometricState newState: Int ): Boolean = when (newState) { STATE_PENDING_CONFIRMATION -> true STATE_AUTHENTICATED -> false - else -> super.shouldAnimateForTransition(oldState, newState) + else -> super.shouldAnimateIconViewForTransition(oldState, newState) } @RawRes diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt index b962cc43eddf..436f9dfb0d74 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt @@ -104,12 +104,14 @@ open class AuthBiometricFingerprintIconController( iconView.frame = 0 iconViewOverlay.frame = 0 - if (shouldAnimateForTransition(lastState, newState)) { + if (shouldAnimateIconViewForTransition(lastState, newState)) { iconView.playAnimation() + } + + if (shouldAnimateIconViewOverlayForTransition(lastState, newState)) { iconViewOverlay.playAnimation() - } else if (lastState == STATE_IDLE && newState == STATE_AUTHENTICATING_ANIMATING_IN) { - iconView.playAnimation() } + LottieColorUtils.applyDynamicColors(context, iconView) LottieColorUtils.applyDynamicColors(context, iconViewOverlay) } @@ -127,7 +129,7 @@ open class AuthBiometricFingerprintIconController( } iconView.frame = 0 - if (shouldAnimateForTransition(lastState, newState)) { + if (shouldAnimateIconViewForTransition(lastState, newState)) { iconView.playAnimation() } LottieColorUtils.applyDynamicColors(context, iconView) @@ -160,7 +162,20 @@ open class AuthBiometricFingerprintIconController( return if (id != null) context.getString(id) else null } - protected open fun shouldAnimateForTransition( + protected open fun shouldAnimateIconViewForTransition( + @BiometricState oldState: Int, + @BiometricState newState: Int + ) = when (newState) { + STATE_HELP, + STATE_ERROR -> true + STATE_AUTHENTICATING_ANIMATING_IN, + STATE_AUTHENTICATING -> + oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE + STATE_AUTHENTICATED -> true + else -> false + } + + protected open fun shouldAnimateIconViewOverlayForTransition( @BiometricState oldState: Int, @BiometricState newState: Int ) = when (newState) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 092339a74e3f..2dc0cd34adf4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; @@ -76,6 +77,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.data.repository.BiometricType; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.VibratorHelper; @@ -85,8 +87,10 @@ import com.android.systemui.util.concurrency.Execution; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -150,6 +154,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps; @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps; + @NonNull private final Map<Integer, Boolean> mFpEnrolledForUser = new HashMap<>(); @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; @@ -161,7 +166,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); - private final VibratorHelper mVibratorHelper; private void vibrateSuccess(int modality) { @@ -331,27 +335,35 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mExecution.assertIsMainThread(); Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId + ", hasEnrollments: " + hasEnrollments); - if (mUdfpsProps == null) { - Log.d(TAG, "handleEnrollmentsChanged, mUdfpsProps is null"); - } else { - for (FingerprintSensorPropertiesInternal prop : mUdfpsProps) { + BiometricType sensorBiometricType = BiometricType.UNKNOWN; + if (mFpProps != null) { + for (FingerprintSensorPropertiesInternal prop: mFpProps) { if (prop.sensorId == sensorId) { - mUdfpsEnrolledForUser.put(userId, hasEnrollments); + mFpEnrolledForUser.put(userId, hasEnrollments); + if (prop.isAnyUdfpsType()) { + sensorBiometricType = BiometricType.UNDER_DISPLAY_FINGERPRINT; + mUdfpsEnrolledForUser.put(userId, hasEnrollments); + } else if (prop.isAnySidefpsType()) { + sensorBiometricType = BiometricType.SIDE_FINGERPRINT; + mSfpsEnrolledForUser.put(userId, hasEnrollments); + } else if (prop.sensorType == TYPE_REAR) { + sensorBiometricType = BiometricType.REAR_FINGERPRINT; + } + break; } } } - - if (mSidefpsProps == null) { - Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null"); - } else { - for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) { + if (mFaceProps != null && sensorBiometricType == BiometricType.UNKNOWN) { + for (FaceSensorPropertiesInternal prop : mFaceProps) { if (prop.sensorId == sensorId) { - mSfpsEnrolledForUser.put(userId, hasEnrollments); + sensorBiometricType = BiometricType.FACE; + break; } } } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(); + cb.onEnrollmentsChanged(sensorBiometricType, userId, hasEnrollments); } } @@ -604,6 +616,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } + /** Get FP sensor properties */ + public @Nullable List<FingerprintSensorPropertiesInternal> getFingerprintProperties() { + return mFpProps; + } + /** * @return where the face sensor exists in pixels in the current device orientation. Returns * null if no face sensor exists. @@ -828,7 +845,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } @Override - public void setBiometicContextListener(IBiometricContextListener listener) { + public void setBiometricContextListener(IBiometricContextListener listener) { mBiometricContextListener = listener; notifyDozeChanged(mStatusBarStateController.isDozing(), mWakefulnessLifecycle.getWakefulness()); @@ -1081,6 +1098,13 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mSfpsEnrolledForUser.get(userId); } + /** + * Whether the passed userId has enrolled at least one fingerprint. + */ + public boolean isFingerprintEnrolled(int userId) { + return mFpEnrolledForUser.getOrDefault(userId, false); + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; @@ -1263,6 +1287,16 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, default void onEnrollmentsChanged() {} /** + * Called when UDFPS enrollments have changed. This is called after boot and on changes to + * enrollment. + */ + default void onEnrollmentsChanged( + @NonNull BiometricType biometricType, + int userId, + boolean hasEnrollments + ) {} + + /** * Called when the biometric prompt starts showing. */ default void onBiometricPromptShown() {} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java new file mode 100644 index 000000000000..902bb18d17b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +/** Provides the status of the interactive to auth feature. */ +public interface FingerprintInteractiveToAuthProvider { + /** + * + * @param userId the user Id. + * @return true if the InteractiveToAuthFeature is enabled, false if disabled. + */ + boolean isEnabled(int userId); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 1afa9b26ce98..6f594d5eb0e2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -111,7 +111,7 @@ constructor( context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() private val isReverseDefaultRotation = - context.getResources().getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) + context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) private var overlayHideAnimator: ViewPropertyAnimator? = null @@ -268,10 +268,12 @@ constructor( val isDefaultOrientation = if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation val size = windowManager.maximumWindowMetrics.bounds + val displayWidth = if (isDefaultOrientation) size.width() else size.height() val displayHeight = if (isDefaultOrientation) size.height() else size.width() val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() + val sensorBounds = if (overlayOffsets.isYAligned()) { Rect( @@ -297,6 +299,7 @@ constructor( overlayViewParams.x = sensorBounds.left overlayViewParams.y = sensorBounds.top + windowManager.updateViewLayout(overlayView, overlayViewParams) } @@ -306,7 +309,12 @@ constructor( } // hide after a few seconds if the sensor is oriented down and there are // large overlapping system bars - val rotation = context.display?.rotation + var rotation = context.display?.rotation + + if (rotation != null) { + rotation = getRotationFromDefault(rotation) + } + if ( windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar() && ((rotation == Surface.ROTATION_270 && overlayOffsets.isYAligned()) || diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt index 4130cf589310..ef7dcb7aac93 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -190,11 +190,6 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( open fun listenForTouchesOutsideView(): Boolean = false /** - * Called on touches outside of the view if listenForTouchesOutsideView returns true - */ - open fun onTouchOutsideView() {} - - /** * Called when a view should announce an accessibility event. */ open fun doAnnounceForAccessibility(str: String) {} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index f3136babada6..3a9706da9090 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -73,6 +73,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -149,6 +150,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Nullable private final TouchProcessor mTouchProcessor; + @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -220,6 +222,10 @@ public class UdfpsController implements DozeReceiver, Dumpable { @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mSensorProps=(" + mSensorProps + ")"); + pw.println("Using new touch detection framework: " + mFeatureFlags.isEnabled( + Flags.UDFPS_NEW_TOUCH_DETECTION)); + pw.println("Using ellipse touch detection: " + mFeatureFlags.isEnabled( + Flags.UDFPS_ELLIPSE_DETECTION)); } public class UdfpsOverlayController extends IUdfpsOverlayController.Stub { @@ -232,12 +238,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { mShadeExpansionStateManager, mKeyguardViewManager, mKeyguardUpdateMonitor, mDialogManager, mDumpManager, mLockscreenShadeTransitionController, mConfigurationController, - mSystemClock, mKeyguardStateController, + mKeyguardStateController, mUnlockedScreenOffAnimationController, mUdfpsDisplayMode, requestId, reason, callback, (view, event, fromUdfpsView) -> onTouch(requestId, event, fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags, - mPrimaryBouncerInteractor))); + mPrimaryBouncerInteractor, mAlternateBouncerInteractor))); } @Override @@ -329,13 +335,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOverlayParams.equals(overlayParams)) { mOverlayParams = overlayParams; - final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer(); + final boolean wasShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState(); // When the bounds change it's always necessary to re-create the overlay's window with // new LayoutParams. If the overlay needs to be shown, this will re-create and show the // overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden. redrawOverlay(); - if (wasShowingAltAuth) { + if (wasShowingAlternateBouncer) { mKeyguardViewManager.showBouncer(true); } } @@ -543,9 +549,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { final UdfpsView udfpsView = mOverlay.getOverlayView(); boolean handled = false; switch (event.getActionMasked()) { - case MotionEvent.ACTION_OUTSIDE: - udfpsView.onTouchOutsideView(); - return true; case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_HOVER_ENTER: Trace.beginSection("UdfpsController.onTouch.ACTION_DOWN"); @@ -719,7 +722,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider, @NonNull @BiometricsBackground Executor biometricsExecutor, @NonNull PrimaryBouncerInteractor primaryBouncerInteractor, - @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) { + @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor, + @NonNull AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -759,6 +763,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mBiometricExecutor = biometricsExecutor; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mAlternateBouncerInteractor = alternateBouncerInteractor; mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) ? singlePointerTouchProcessor : null; @@ -853,9 +858,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { onFingerUp(mOverlay.getRequestId(), oldView); } final boolean removed = mOverlay.hide(); - if (mKeyguardViewManager.isShowingAlternateBouncer()) { - mKeyguardViewManager.hideAlternateBouncer(true); - } + mKeyguardViewManager.hideAlternateBouncer(true); Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed); } else { Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 8db4927ee059..a3c4985fd5cc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -50,6 +50,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -59,7 +60,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" @@ -86,7 +86,6 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val dumpManager: DumpManager, private val transitionController: LockscreenShadeTransitionController, private val configurationController: ConfigurationController, - private val systemClock: SystemClock, private val keyguardStateController: KeyguardStateController, private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider, @@ -97,7 +96,8 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val activityLaunchAnimator: ActivityLaunchAnimator, private val featureFlags: FeatureFlags, private val primaryBouncerInteractor: PrimaryBouncerInteractor, - private val isDebuggable: Boolean = Build.IS_DEBUGGABLE + private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -255,14 +255,14 @@ class UdfpsControllerOverlay @JvmOverloads constructor( dumpManager, transitionController, configurationController, - systemClock, keyguardStateController, unlockedScreenOffAnimationController, dialogManager, controller, activityLaunchAnimator, featureFlags, - primaryBouncerInteractor + primaryBouncerInteractor, + alternateBouncerInteractor, ) } REASON_AUTH_BP -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt index 63144fcea761..63a1b76b8103 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt @@ -31,7 +31,10 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionListener @@ -39,16 +42,14 @@ import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackStateAnimator -import com.android.systemui.statusbar.phone.KeyguardBouncer -import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateBouncer import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -65,25 +66,27 @@ constructor( dumpManager: DumpManager, private val lockScreenShadeTransitionController: LockscreenShadeTransitionController, private val configurationController: ConfigurationController, - private val systemClock: SystemClock, private val keyguardStateController: KeyguardStateController, private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, systemUIDialogManager: SystemUIDialogManager, private val udfpsController: UdfpsController, private val activityLaunchAnimator: ActivityLaunchAnimator, featureFlags: FeatureFlags, - private val primaryBouncerInteractor: PrimaryBouncerInteractor + private val primaryBouncerInteractor: PrimaryBouncerInteractor, + private val alternateBouncerInteractor: AlternateBouncerInteractor, ) : UdfpsAnimationViewController<UdfpsKeyguardView>( view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, - dumpManager + dumpManager, ) { private val useExpandedOverlay: Boolean = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER) + private val isModernAlternateBouncerEnabled: Boolean = + featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER) private var showingUdfpsBouncer = false private var udfpsRequested = false private var qsExpansion = 0f @@ -91,7 +94,6 @@ constructor( private var statusBarState = 0 private var transitionToFullShadeProgress = 0f private var lastDozeAmount = 0f - private var lastUdfpsBouncerShowTime: Long = -1 private var panelExpansionFraction = 0f private var launchTransitionFadingAway = false private var isLaunchingActivity = false @@ -110,10 +112,10 @@ constructor( } /** * Hidden amount of input (pin/pattern/password) bouncer. This is used - * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only - * used for the non-modernBouncer. + * [KeyguardBouncerConstants.EXPANSION_VISIBLE] (0f) to + * [KeyguardBouncerConstants.EXPANSION_HIDDEN] (1f). Only used for the non-modernBouncer. */ - private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN + private var inputBouncerHiddenAmount = KeyguardBouncerConstants.EXPANSION_HIDDEN private var inputBouncerExpansion = 0f // only used for modernBouncer private val stateListener: StatusBarStateController.StateListener = @@ -244,20 +246,8 @@ constructor( } } - private val mAlternateBouncer: AlternateBouncer = - object : AlternateBouncer { - override fun showAlternateBouncer(): Boolean { - return showUdfpsBouncer(true) - } - - override fun hideAlternateBouncer(): Boolean { - return showUdfpsBouncer(false) - } - - override fun isShowingAlternateBouncer(): Boolean { - return showingUdfpsBouncer - } - + private val occludingAppBiometricUI: OccludingAppBiometricUI = + object : OccludingAppBiometricUI { override fun requestUdfps(request: Boolean, color: Int) { udfpsRequested = request view.requestUdfps(request, color) @@ -275,16 +265,19 @@ constructor( override fun onInit() { super.onInit() - keyguardViewManager.setAlternateBouncer(mAlternateBouncer) + keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) } init { - if (isModernBouncerEnabled) { + if (isModernBouncerEnabled || isModernAlternateBouncerEnabled) { view.repeatWhenAttached { // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion // can make the view not visible; and we still want to listen for events // that may make the view visible again. - repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) } + repeatOnLifecycle(Lifecycle.State.CREATED) { + if (isModernBouncerEnabled) listenForBouncerExpansion(this) + if (isModernAlternateBouncerEnabled) listenForAlternateBouncerVisibility(this) + } } } } @@ -300,8 +293,18 @@ constructor( } } + @VisibleForTesting + internal suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job { + return scope.launch { + alternateBouncerInteractor.isVisible.collect { isVisible: Boolean -> + showUdfpsBouncer(isVisible) + } + } + } + public override fun onViewAttached() { super.onViewAttached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(true) val dozeAmount = statusBarStateController.dozeAmount lastDozeAmount = dozeAmount stateListener.onDozeAmountChanged(dozeAmount, dozeAmount) @@ -326,7 +329,8 @@ constructor( view.updatePadding() updateAlpha() updatePauseAuth() - keyguardViewManager.setAlternateBouncer(mAlternateBouncer) + keyguardViewManager.setLegacyAlternateBouncer(legacyAlternateBouncer) + keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) lockScreenShadeTransitionController.udfpsKeyguardViewController = this activityLaunchAnimator.addListener(activityLaunchAnimatorListener) view.mUseExpandedOverlay = useExpandedOverlay @@ -334,10 +338,12 @@ constructor( override fun onViewDetached() { super.onViewDetached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(false) faceDetectRunning = false keyguardStateController.removeCallback(keyguardStateControllerCallback) statusBarStateController.removeCallback(stateListener) - keyguardViewManager.removeAlternateAuthInterceptor(mAlternateBouncer) + keyguardViewManager.removeLegacyAlternateBouncer(legacyAlternateBouncer) + keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI) keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) configurationController.removeCallback(configurationListener) shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener) @@ -356,7 +362,16 @@ constructor( override fun dump(pw: PrintWriter, args: Array<String>) { super.dump(pw, args) pw.println("isModernBouncerEnabled=$isModernBouncerEnabled") + pw.println("isModernAlternateBouncerEnabled=$isModernAlternateBouncerEnabled") pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer") + pw.println( + "altBouncerInteractor#isAlternateBouncerVisible=" + + "${alternateBouncerInteractor.isVisibleState()}" + ) + pw.println( + "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" + + "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}" + ) pw.println("faceDetectRunning=$faceDetectRunning") pw.println("statusBarState=" + StatusBarState.toString(statusBarState)) pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress") @@ -385,9 +400,6 @@ constructor( val udfpsAffordanceWasNotShowing = shouldPauseAuth() showingUdfpsBouncer = show if (showingUdfpsBouncer) { - lastUdfpsBouncerShowTime = systemClock.uptimeMillis() - } - if (showingUdfpsBouncer) { if (udfpsAffordanceWasNotShowing) { view.animateInUdfpsBouncer(null) } @@ -452,7 +464,7 @@ constructor( return if (isModernBouncerEnabled) { inputBouncerExpansion == 1f } else { - keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateBouncer + keyguardViewManager.isBouncerShowing && !alternateBouncerInteractor.isVisibleState() } } @@ -460,30 +472,6 @@ constructor( return true } - override fun onTouchOutsideView() { - maybeShowInputBouncer() - } - - /** - * If we were previously showing the udfps bouncer, hide it and instead show the regular - * (pin/pattern/password) bouncer. - * - * Does nothing if we weren't previously showing the UDFPS bouncer. - */ - private fun maybeShowInputBouncer() { - if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) { - keyguardViewManager.showPrimaryBouncer(true) - } - } - - /** - * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the - * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer. - */ - private fun hasUdfpsBouncerShownWithMinTime(): Boolean { - return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200 - } - /** * Set the progress we're currently transitioning to the full shade. 0.0f means we're not * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down @@ -545,7 +533,7 @@ constructor( if (isModernBouncerEnabled) { return } - val altBouncerShowing = keyguardViewManager.isShowingAlternateBouncer + val altBouncerShowing = alternateBouncerInteractor.isVisibleState() if (altBouncerShowing || !keyguardViewManager.primaryBouncerIsOrWillBeShowing()) { inputBouncerHiddenAmount = 1f } else if (keyguardViewManager.isBouncerShowing) { @@ -554,6 +542,21 @@ constructor( } } + private val legacyAlternateBouncer: LegacyAlternateBouncer = + object : LegacyAlternateBouncer { + override fun showAlternateBouncer(): Boolean { + return showUdfpsBouncer(true) + } + + override fun hideAlternateBouncer(): Boolean { + return showUdfpsBouncer(false) + } + + override fun isShowingAlternateBouncer(): Boolean { + return showingUdfpsBouncer + } + } + companion object { const val TAG = "UdfpsKeyguardViewController" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt index 4a8877edfa53..e61c614f0292 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt @@ -111,10 +111,6 @@ class UdfpsView( } } - fun onTouchOutsideView() { - animationViewController?.onTouchOutsideView() - } - override fun onAttachedToWindow() { super.onAttachedToWindow() Log.v(TAG, "onAttachedToWindow") diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt index 857224290752..682d38a8f1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.udfps import android.graphics.Point import android.graphics.Rect +import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import kotlin.math.cos import kotlin.math.pow @@ -50,7 +51,8 @@ class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetecto return result <= 1 } - private fun calculateSensorPoints(sensorBounds: Rect): List<Point> { + @VisibleForTesting + fun calculateSensorPoints(sensorBounds: Rect): List<Point> { val sensorX = sensorBounds.centerX() val sensorY = sensorBounds.centerY() val cornerOffset: Int = sensorBounds.width() / 4 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt index 62bedc627b07..48d48450e13e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt @@ -26,28 +26,28 @@ data class NormalizedTouchData( * Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID * is not available. */ - val pointerId: Int, + val pointerId: Int = MotionEvent.INVALID_POINTER_ID, /** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */ - val x: Float, + val x: Float = 0f, /** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */ - val y: Float, + val y: Float = 0f, /** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */ - val minor: Float, + val minor: Float = 0f, /** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */ - val major: Float, + val major: Float = 0f, /** [MotionEvent.getOrientation] mapped to natural orientation. */ - val orientation: Float, + val orientation: Float = 0f, /** [MotionEvent.getEventTime]. */ - val time: Long, + val time: Long = 0, /** [MotionEvent.getDownTime]. */ - val gestureStart: Long, + val gestureStart: Long = 0, ) { /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 338bf66d197e..3a01cd502929 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -27,6 +27,8 @@ import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject +private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270) + /** * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. */ @@ -41,74 +43,72 @@ class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: Overl ): TouchProcessorResult { fun preprocess(): PreprocessedTouch { - // TODO(b/253085297): Add multitouch support. pointerIndex can be > 0 for ACTION_MOVE. - val pointerIndex = 0 - val touchData = event.normalize(pointerIndex, overlayParams) - val isGoodOverlap = - overlapDetector.isGoodOverlap(touchData, overlayParams.nativeSensorBounds) - return PreprocessedTouch(touchData, previousPointerOnSensorId, isGoodOverlap) + val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) } + val pointersOnSensor = + touchData + .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) } + .map { it.pointerId } + return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor) } return when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> processActionDown(preprocess()) + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_MOVE -> processActionMove(preprocess()) - MotionEvent.ACTION_UP -> processActionUp(preprocess()) - MotionEvent.ACTION_CANCEL -> - processActionCancel(event.normalize(pointerIndex = 0, overlayParams)) + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> + processActionUp(preprocess(), event.getPointerId(event.actionIndex)) + MotionEvent.ACTION_CANCEL -> processActionCancel(NormalizedTouchData()) else -> Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked)) } } } +/** + * [data] contains a list of NormalizedTouchData for pointers in the motionEvent ordered by + * pointerIndex + * + * [previousPointerOnSensorId] the pointerId of the previous pointer on the sensor, + * [MotionEvent.INVALID_POINTER_ID] if none + * + * [pointersOnSensor] contains a list of ids of pointers on the sensor + */ private data class PreprocessedTouch( - val data: NormalizedTouchData, + val data: List<NormalizedTouchData>, val previousPointerOnSensorId: Int, - val isGoodOverlap: Boolean, + val pointersOnSensor: List<Int>, ) -private fun processActionDown(touch: PreprocessedTouch): TouchProcessorResult { - return if (touch.isGoodOverlap) { - ProcessedTouch(InteractionEvent.DOWN, pointerOnSensorId = touch.data.pointerId, touch.data) - } else { - val event = - if (touch.data.pointerId == touch.previousPointerOnSensorId) { - InteractionEvent.UP - } else { - InteractionEvent.UNCHANGED - } - ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data) - } -} - private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult { val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID - val interactionEvent = - when { - touch.isGoodOverlap && !hadPointerOnSensor -> InteractionEvent.DOWN - !touch.isGoodOverlap && hadPointerOnSensor -> InteractionEvent.UP - else -> InteractionEvent.UNCHANGED - } - val pointerOnSensorId = - when (interactionEvent) { - InteractionEvent.UNCHANGED -> touch.previousPointerOnSensorId - InteractionEvent.DOWN -> touch.data.pointerId - else -> INVALID_POINTER_ID - } - return ProcessedTouch(interactionEvent, pointerOnSensorId, touch.data) + val hasPointerOnSensor = touch.pointersOnSensor.isNotEmpty() + val pointerOnSensorId = touch.pointersOnSensor.firstOrNull() ?: INVALID_POINTER_ID + + return if (!hadPointerOnSensor && hasPointerOnSensor) { + val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData() + ProcessedTouch(InteractionEvent.DOWN, data.pointerId, data) + } else if (hadPointerOnSensor && !hasPointerOnSensor) { + ProcessedTouch(InteractionEvent.UP, INVALID_POINTER_ID, NormalizedTouchData()) + } else { + val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData() + ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data) + } } -private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult { - return if (touch.isGoodOverlap) { - ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data) +private fun processActionUp(touch: PreprocessedTouch, actionId: Int): TouchProcessorResult { + // Finger lifted and it was the only finger on the sensor + return if (touch.pointersOnSensor.size == 1 && touch.pointersOnSensor.contains(actionId)) { + ProcessedTouch( + InteractionEvent.UP, + pointerOnSensorId = INVALID_POINTER_ID, + NormalizedTouchData() + ) } else { - val event = - if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) { - InteractionEvent.UP - } else { - InteractionEvent.UNCHANGED - } - ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + // Pick new pointerOnSensor that's not the finger that was lifted + val pointerOnSensorId = touch.pointersOnSensor.find { it != actionId } ?: INVALID_POINTER_ID + val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData() + ProcessedTouch(InteractionEvent.UNCHANGED, data.pointerId, data) } } @@ -129,19 +129,27 @@ private fun MotionEvent.normalize( val nativeY = naturalTouch.y / overlayParams.scaleFactor val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + var nativeOrientation: Float = getOrientation(pointerIndex) + if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) { + nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat() + } return NormalizedTouchData( pointerId = getPointerId(pointerIndex), x = nativeX, y = nativeY, minor = nativeMinor, major = nativeMajor, - // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O. - orientation = getOrientation(pointerIndex), + orientation = nativeOrientation, time = eventTime, gestureStart = downTime, ) } +private fun toRadVerticalFromRotated(rad: Double): Double { + val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI + return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI +} + /** * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device * is in the [Surface.ROTATION_0] orientation. @@ -152,7 +160,7 @@ private fun MotionEvent.rotateToNaturalOrientation( ): PointF { val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) val rot = overlayParams.rotation - if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + if (SUPPORTED_ROTATIONS.contains(rot)) { RotationUtils.rotatePointF( touchPoint, RotationUtils.deltaRotation(rot, Surface.ROTATION_0), diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt index 867faf9843fe..cc43e7ed25a5 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt @@ -20,15 +20,13 @@ import android.content.Context import android.content.Intent import android.provider.MediaStore import android.text.TextUtils - import com.android.systemui.R class CameraIntents { companion object { - val DEFAULT_SECURE_CAMERA_INTENT_ACTION = - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE - val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + val DEFAULT_SECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE + val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" @JvmStatic @@ -44,18 +42,14 @@ class CameraIntents { @JvmStatic fun getInsecureCameraIntent(context: Context): Intent { val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { - intent.setPackage(it) - } + getOverrideCameraPackage(context)?.let { intent.setPackage(it) } return intent } @JvmStatic fun getSecureCameraIntent(context: Context): Intent { val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { - intent.setPackage(it) - } + getOverrideCameraPackage(context)?.let { intent.setPackage(it) } return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) } @@ -68,5 +62,11 @@ class CameraIntents { fun isInsecureCameraIntent(intent: Intent?): Boolean { return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false } + + /** Returns an [Intent] that can be used to start the camera in video mode. */ + @JvmStatic + fun getVideoCameraIntent(): Intent { + return Intent(VIDEO_CAMERA_INTENT_ACTION) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt index cf02f8fb4a3c..a434617f2da7 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt @@ -21,7 +21,9 @@ import android.content.Intent import javax.inject.Inject /** Injectable wrapper around [CameraIntents]. */ -class CameraIntentsWrapper @Inject constructor( +class CameraIntentsWrapper +@Inject +constructor( private val context: Context, ) { @@ -40,4 +42,9 @@ class CameraIntentsWrapper @Inject constructor( fun getInsecureCameraIntent(): Intent { return CameraIntents.getInsecureCameraIntent(context) } + + /** Returns an [Intent] that can be used to start the camera in video mode. */ + fun getVideoCameraIntent(): Intent { + return CameraIntents.getVideoCameraIntent() + } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index e8e1f2e95f5d..e9ac840cf4f4 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -176,7 +176,8 @@ public class BrightLineFalsingManager implements FalsingManager { private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC; @Inject - public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, + public BrightLineFalsingManager( + FalsingDataProvider falsingDataProvider, MetricsLogger metricsLogger, @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier, @@ -399,7 +400,9 @@ public class BrightLineFalsingManager implements FalsingManager { || mDataProvider.isJustUnlockedWithFace() || mDataProvider.isDocked() || mAccessibilityManager.isTouchExplorationEnabled() - || mDataProvider.isA11yAction(); + || mDataProvider.isA11yAction() + || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED) + && !mDataProvider.isFolded()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 09ebeeac163f..5f347c158818 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; @@ -42,6 +43,7 @@ public class FalsingDataProvider { private final int mWidthPixels; private final int mHeightPixels; private BatteryController mBatteryController; + private final FoldStateListener mFoldStateListener; private final DockManager mDockManager; private final float mXdpi; private final float mYdpi; @@ -65,12 +67,14 @@ public class FalsingDataProvider { public FalsingDataProvider( DisplayMetrics displayMetrics, BatteryController batteryController, + FoldStateListener foldStateListener, DockManager dockManager) { mXdpi = displayMetrics.xdpi; mYdpi = displayMetrics.ydpi; mWidthPixels = displayMetrics.widthPixels; mHeightPixels = displayMetrics.heightPixels; mBatteryController = batteryController; + mFoldStateListener = foldStateListener; mDockManager = dockManager; FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi()); @@ -376,6 +380,10 @@ public class FalsingDataProvider { return mBatteryController.isWirelessCharging() || mDockManager.isDocked(); } + public boolean isFolded() { + return Boolean.TRUE.equals(mFoldStateListener.getFolded()); + } + /** Implement to be alerted abotu the beginning and ending of falsing tracking. */ public interface SessionListener { /** Called when the lock screen is shown and falsing-tracking begins. */ diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt index f95a8ee89a2c..7bbfec7df9d8 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt @@ -28,7 +28,6 @@ class LaunchableImageView : ImageView, LaunchableView { LaunchableViewDelegate( this, superSetVisibility = { super.setVisibility(it) }, - superSetTransitionVisibility = { super.setTransitionVisibility(it) }, ) constructor(context: Context?) : super(context) @@ -53,8 +52,4 @@ class LaunchableImageView : ImageView, LaunchableView { override fun setVisibility(visibility: Int) { delegate.setVisibility(visibility) } - - override fun setTransitionVisibility(visibility: Int) { - delegate.setTransitionVisibility(visibility) - } } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt index c27b82aeeb47..ddde6280f3a2 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt @@ -28,7 +28,6 @@ class LaunchableLinearLayout : LinearLayout, LaunchableView { LaunchableViewDelegate( this, superSetVisibility = { super.setVisibility(it) }, - superSetTransitionVisibility = { super.setTransitionVisibility(it) }, ) constructor(context: Context?) : super(context) @@ -53,8 +52,4 @@ class LaunchableLinearLayout : LinearLayout, LaunchableView { override fun setVisibility(visibility: Int) { delegate.setVisibility(visibility) } - - override fun setTransitionVisibility(visibility: Int) { - delegate.setTransitionVisibility(visibility) - } } diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index e5ec727f0437..c0f854958c41 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -17,8 +17,12 @@ package com.android.systemui.compose +import android.content.Context +import android.view.View import androidx.activity.ComponentActivity +import androidx.lifecycle.LifecycleOwner import com.android.systemui.people.ui.viewmodel.PeopleViewModel +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel /** * A facade to interact with Compose, when it is available. @@ -35,10 +39,22 @@ interface BaseComposeFacade { */ fun isComposeAvailable(): Boolean + /** + * Return the [ComposeInitializer] to make Compose usable in windows outside normal activities. + */ + fun composeInitializer(): ComposeInitializer + /** Bind the content of [activity] to [viewModel]. */ fun setPeopleSpaceActivityContent( activity: ComponentActivity, viewModel: PeopleViewModel, onResult: (PeopleViewModel.Result) -> Unit, ) + + /** Create a [View] to represent [viewModel] on screen. */ + fun createFooterActionsView( + context: Context, + viewModel: FooterActionsViewModel, + qsVisibilityLifecycleOwner: LifecycleOwner, + ): View } diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt new file mode 100644 index 000000000000..90dc3a00daa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt @@ -0,0 +1,48 @@ +/* + * 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.compose + +import android.view.View + +/** + * An initializer to use Compose outside of an Activity, e.g. inside a window added directly using + * [android.view.WindowManager.addView] (like the shade or status bar) or inside a dialog. + * + * Example: + * ``` + * windowManager.addView(MyWindowRootView(context), /* layoutParams */) + * + * class MyWindowRootView(context: Context) : FrameLayout(context) { + * override fun onAttachedToWindow() { + * super.onAttachedToWindow() + * ComposeInitializer.onAttachedToWindow(this) + * } + * + * override fun onDetachedFromWindow() { + * super.onDetachedFromWindow() + * ComposeInitializer.onDetachedFromWindow(this) + * } + * } + * ``` + */ +interface ComposeInitializer { + /** Function to be called on your window root view's [View.onAttachedToWindow] function. */ + fun onAttachedToWindow(root: View) + + /** Function to be called on your window root view's [View.onDetachedFromWindow] function. */ + fun onDetachedFromWindow(root: View) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt index eed55315e836..9b2a224f17e0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt @@ -51,13 +51,22 @@ interface ControlsBindingController : UserAwareController { fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback) /** - * Request to bind to the given service. + * Request to bind to the given service. This should only be used for services using the full + * [ControlsProviderService] API, where SystemUI renders the devices' UI. * * @param component The [ComponentName] of the service to bind */ fun bindService(component: ComponentName) /** + * Bind to a service that provides a Device Controls panel (embedded activity). This will allow + * the app to remain "warm", and reduce latency. + * + * @param component The [ComponentName] of the [ControlsProviderService] to bind. + */ + fun bindServiceForPanel(component: ComponentName) + + /** * Send a subscribe message to retrieve status of a set of controls. * * @param structureInfo structure containing the controls to update diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 2f0fd99337e5..3d6d3356fb55 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -170,6 +170,10 @@ open class ControlsBindingControllerImpl @Inject constructor( retrieveLifecycleManager(component).bindService() } + override fun bindServiceForPanel(component: ComponentName) { + retrieveLifecycleManager(component).bindServiceForPanel() + } + override fun changeUser(newUser: UserHandle) { if (newUser == currentUser) return diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 2f49c3fe863e..f29f6d0dd0cb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -189,6 +189,14 @@ interface ControlsController : UserAwareController { fun getPreferredSelection(): SelectedItem /** + * Bind to a service that provides a Device Controls panel (embedded activity). This will allow + * the app to remain "warm", and reduce latency. + * + * @param component The [ComponentName] of the [ControlsProviderService] to bind. + */ + fun bindComponentForPanel(componentName: ComponentName) + + /** * Interface for structure to pass data to [ControlsFavoritingActivity]. */ interface LoadData { diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 80c5f661f9a3..111fcbbe30be 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -477,6 +477,10 @@ class ControlsControllerImpl @Inject constructor ( bindingController.unsubscribe() } + override fun bindComponentForPanel(componentName: ComponentName) { + bindingController.bindServiceForPanel(componentName) + } + override fun addFavorite( componentName: ComponentName, structureName: CharSequence, diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt index 5b38e5b28be9..72c3a943c30b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -78,6 +78,10 @@ class ControlsProviderLifecycleManager( private const val DEBUG = true private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or Context.BIND_NOT_PERCEPTIBLE + // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI. + // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app + // once the Task is finished in the device controls panel. + private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE } private val intent = Intent().apply { @@ -87,18 +91,19 @@ class ControlsProviderLifecycleManager( }) } - private fun bindService(bind: Boolean) { + private fun bindService(bind: Boolean, forPanel: Boolean = false) { executor.execute { requiresBound = bind if (bind) { - if (bindTryCount != MAX_BIND_RETRIES) { + if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) { if (DEBUG) { Log.d(TAG, "Binding service $intent") } bindTryCount++ try { + val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS val bound = context - .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user) + .bindServiceAsUser(intent, serviceConnection, flags, user) if (!bound) { context.unbindService(serviceConnection) } @@ -279,6 +284,10 @@ class ControlsProviderLifecycleManager( bindService(true) } + fun bindServiceForPanel() { + bindService(bind = true, forPanel = true) + } + /** * Request unbind from the service. */ diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 1e3e5cd1c31c..6289788f650a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -232,6 +232,8 @@ class ControlsUiControllerImpl @Inject constructor ( ControlKey(selected.structure.componentName, it.ci.controlId) } controlsController.get().subscribeToFavorites(selected.structure) + } else { + controlsController.get().bindComponentForPanel(selected.componentName) } listingCallback = createCallback(::showControlsView) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 8012dea643fb..8e9992fdd296 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.log.SessionTracker +import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI import com.android.systemui.media.RingtonePlayer import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver @@ -217,6 +218,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(ToastUI::class) abstract fun bindToastUI(service: ToastUI): CoreStartable + /** Inject into MediaOutputSwitcherDialogUI. */ + @Binds + @IntoMap + @ClassKey(MediaOutputSwitcherDialogUI::class) + abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable + /** Inject into VolumeUI. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index b8e66735c740..2d0dfa1d921b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -31,6 +31,7 @@ import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; @@ -53,6 +54,7 @@ import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; +import com.android.systemui.plugins.BcSmartspaceConfigPlugin; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; import com.android.systemui.qs.FgsManagerController; @@ -210,6 +212,9 @@ public abstract class SystemUIModule { abstract BcSmartspaceDataPlugin optionalBcSmartspaceDataPlugin(); @BindsOptionalOf + abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin(); + + @BindsOptionalOf abstract Recents optionalRecents(); @BindsOptionalOf @@ -221,6 +226,9 @@ public abstract class SystemUIModule { @BindsOptionalOf abstract AlternateUdfpsTouchProvider optionalUdfpsTouchProvider(); + @BindsOptionalOf + abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider(); + @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 37183e89df29..2ef1262fe55e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -116,6 +116,7 @@ public class DozeSensors { private boolean mListening; private boolean mListeningTouchScreenSensors; private boolean mListeningProxSensors; + private boolean mListeningAodOnlySensors; private boolean mUdfpsEnrolled; @DevicePostureController.DevicePostureInt @@ -184,7 +185,8 @@ public class DozeSensors { dozeParameters.getPulseOnSigMotion(), DozeLog.PULSE_REASON_SENSOR_SIGMOTION, false /* touchCoords */, - false /* touchscreen */), + false /* touchscreen */ + ), new TriggerSensor( mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE), Settings.Secure.DOZE_PICK_UP_GESTURE, @@ -195,14 +197,17 @@ public class DozeSensors { false /* touchscreen */, false /* ignoresSetting */, false /* requires prox */, - true /* immediatelyReRegister */), + true /* immediatelyReRegister */, + false /* requiresAod */ + ), new TriggerSensor( findSensor(config.doubleTapSensorType()), Settings.Secure.DOZE_DOUBLE_TAP_GESTURE, true /* configured */, DozeLog.REASON_SENSOR_DOUBLE_TAP, dozeParameters.doubleTapReportsTouchCoordinates(), - true /* touchscreen */), + true /* touchscreen */ + ), new TriggerSensor( findSensors(config.tapSensorTypeMapping()), Settings.Secure.DOZE_TAP_SCREEN_GESTURE, @@ -214,7 +219,9 @@ public class DozeSensors { false /* ignoresSetting */, dozeParameters.singleTapUsesProx(mDevicePosture) /* requiresProx */, true /* immediatelyReRegister */, - mDevicePosture), + mDevicePosture, + false + ), new TriggerSensor( findSensor(config.longPressSensorType()), Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, @@ -225,7 +232,9 @@ public class DozeSensors { true /* touchscreen */, false /* ignoresSetting */, dozeParameters.longPressUsesProx() /* requiresProx */, - true /* immediatelyReRegister */), + true /* immediatelyReRegister */, + false /* requiresAod */ + ), new TriggerSensor( findSensor(config.udfpsLongPressSensorType()), "doze_pulse_on_auth", @@ -236,7 +245,9 @@ public class DozeSensors { true /* touchscreen */, false /* ignoresSetting */, dozeParameters.longPressUsesProx(), - false /* immediatelyReRegister */), + false /* immediatelyReRegister */, + true /* requiresAod */ + ), new PluginSensor( new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY), Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE, @@ -244,7 +255,8 @@ public class DozeSensors { && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT), DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE, false /* reports touch coordinates */, - false /* touchscreen */), + false /* touchscreen */ + ), new PluginSensor( new SensorManagerPlugin.Sensor(TYPE_WAKE_LOCK_SCREEN), Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE, @@ -252,7 +264,8 @@ public class DozeSensors { DozeLog.PULSE_REASON_SENSOR_WAKE_REACH, false /* reports touch coordinates */, false /* touchscreen */, - mConfig.getWakeLockScreenDebounce()), + mConfig.getWakeLockScreenDebounce() + ), new TriggerSensor( findSensor(config.quickPickupSensorType()), Settings.Secure.DOZE_QUICK_PICKUP_GESTURE, @@ -263,7 +276,9 @@ public class DozeSensors { false /* requiresTouchscreen */, false /* ignoresSetting */, false /* requiresProx */, - true /* immediatelyReRegister */), + true /* immediatelyReRegister */, + false /* requiresAod */ + ), }; setProxListening(false); // Don't immediately start listening when we register. mProximitySensor.register( @@ -357,29 +372,36 @@ public class DozeSensors { /** * If sensors should be registered and sending signals. */ - public void setListening(boolean listen, boolean includeTouchScreenSensors) { - if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors) { + public void setListening(boolean listen, boolean includeTouchScreenSensors, + boolean includeAodOnlySensors) { + if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors + && mListeningAodOnlySensors == includeAodOnlySensors) { return; } mListening = listen; mListeningTouchScreenSensors = includeTouchScreenSensors; + mListeningAodOnlySensors = includeAodOnlySensors; updateListening(); } /** * If sensors should be registered and sending signals. */ - public void setListening(boolean listen, boolean includeTouchScreenSensors, - boolean lowPowerStateOrOff) { + public void setListeningWithPowerState(boolean listen, boolean includeTouchScreenSensors, + boolean includeAodRequiringSensors, boolean lowPowerStateOrOff) { final boolean shouldRegisterProxSensors = !mSelectivelyRegisterProxSensors || lowPowerStateOrOff; - if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors - && mListeningProxSensors == shouldRegisterProxSensors) { + if (mListening == listen + && mListeningTouchScreenSensors == includeTouchScreenSensors + && mListeningProxSensors == shouldRegisterProxSensors + && mListeningAodOnlySensors == includeAodRequiringSensors + ) { return; } mListening = listen; mListeningTouchScreenSensors = includeTouchScreenSensors; mListeningProxSensors = shouldRegisterProxSensors; + mListeningAodOnlySensors = includeAodRequiringSensors; updateListening(); } @@ -391,7 +413,8 @@ public class DozeSensors { for (TriggerSensor s : mTriggerSensors) { boolean listen = mListening && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors) - && (!s.mRequiresProx || mListeningProxSensors); + && (!s.mRequiresProx || mListeningProxSensors) + && (!s.mRequiresAod || mListeningAodOnlySensors); s.setListening(listen); if (listen) { anyListening = true; @@ -499,6 +522,9 @@ public class DozeSensors { private final boolean mRequiresTouchscreen; private final boolean mRequiresProx; + // Whether the sensor should only register if the device is in AOD + private final boolean mRequiresAod; + // Whether to immediately re-register this sensor after the sensor is triggered. // If false, the sensor registration will be updated on the next AOD state transition. private final boolean mImmediatelyReRegister; @@ -527,7 +553,8 @@ public class DozeSensors { requiresTouchscreen, false /* ignoresSetting */, false /* requiresProx */, - true /* immediatelyReRegister */ + true /* immediatelyReRegister */, + false ); } @@ -541,7 +568,8 @@ public class DozeSensors { boolean requiresTouchscreen, boolean ignoresSetting, boolean requiresProx, - boolean immediatelyReRegister + boolean immediatelyReRegister, + boolean requiresAod ) { this( new Sensor[]{ sensor }, @@ -554,7 +582,8 @@ public class DozeSensors { ignoresSetting, requiresProx, immediatelyReRegister, - DevicePostureController.DEVICE_POSTURE_UNKNOWN + DevicePostureController.DEVICE_POSTURE_UNKNOWN, + requiresAod ); } @@ -569,7 +598,8 @@ public class DozeSensors { boolean ignoresSetting, boolean requiresProx, boolean immediatelyReRegister, - @DevicePostureController.DevicePostureInt int posture + @DevicePostureController.DevicePostureInt int posture, + boolean requiresAod ) { mSensors = sensors; mSetting = setting; @@ -580,6 +610,7 @@ public class DozeSensors { mRequiresTouchscreen = requiresTouchscreen; mIgnoresSetting = ignoresSetting; mRequiresProx = requiresProx; + mRequiresAod = requiresAod; mPosture = posture; mImmediatelyReRegister = immediatelyReRegister; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index b95c3f3c0ee7..b70960832d32 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -111,6 +111,7 @@ public class DozeTriggers implements DozeMachine.Part { private boolean mWantProxSensor; private boolean mWantTouchScreenSensors; private boolean mWantSensors; + private boolean mInAod; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -460,12 +461,19 @@ public class DozeTriggers implements DozeMachine.Part { mDozeSensors.requestTemporaryDisable(); break; case DOZE: + mAodInterruptRunnable = null; + mWantProxSensor = false; + mWantSensors = true; + mWantTouchScreenSensors = true; + mInAod = false; + break; case DOZE_AOD: mAodInterruptRunnable = null; - mWantProxSensor = newState != DozeMachine.State.DOZE; + mWantProxSensor = true; mWantSensors = true; mWantTouchScreenSensors = true; - if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) { + mInAod = true; + if (!sWakeDisplaySensorState) { onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE); } break; @@ -491,7 +499,7 @@ public class DozeTriggers implements DozeMachine.Part { break; default: } - mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors); + mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, mInAod); } private void registerCallbacks() { @@ -510,11 +518,12 @@ public class DozeTriggers implements DozeMachine.Part { private void stopListeningToAllTriggers() { unregisterCallbacks(); - mDozeSensors.setListening(false, false); + mDozeSensors.setListening(false, false, false); mDozeSensors.setProxListening(false); mWantSensors = false; mWantProxSensor = false; mWantTouchScreenSensors = false; + mInAod = false; } @Override @@ -523,7 +532,8 @@ public class DozeTriggers implements DozeMachine.Part { final boolean lowPowerStateOrOff = state == Display.STATE_DOZE || state == Display.STATE_DOZE_SUSPEND || state == Display.STATE_OFF; mDozeSensors.setProxListening(mWantProxSensor && lowPowerStateOrOff); - mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, lowPowerStateOrOff); + mDozeSensors.setListeningWithPowerState(mWantSensors, mWantTouchScreenSensors, + mInAod, lowPowerStateOrOff); if (mAodInterruptRunnable != null && state == Display.STATE_ON) { mAodInterruptRunnable.run(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 3106173d81b2..33c8379d2e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -38,6 +38,7 @@ import com.android.systemui.dreams.complication.ComplicationHostViewController; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.dreams.dagger.DreamOverlayModule; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -85,8 +86,9 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve private boolean mBouncerAnimating; - private final KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback = - new KeyguardBouncer.PrimaryBouncerExpansionCallback() { + private final PrimaryBouncerExpansionCallback + mBouncerExpansionCallback = + new PrimaryBouncerExpansionCallback() { @Override public void onStartingToShow() { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index f244cb009ba4..96bce4cd3cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,9 @@ import android.view.ViewGroup; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.systemui.R; +import com.android.systemui.shared.shadow.DoubleShadowIconDrawable; +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo; +import com.android.systemui.statusbar.AlphaOptimizedImageView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -60,8 +64,15 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public static final int STATUS_ICON_PRIORITY_MODE_ON = 6; private final Map<Integer, View> mStatusIcons = new HashMap<>(); + private Context mContext; private ViewGroup mSystemStatusViewGroup; private ViewGroup mExtraSystemStatusViewGroup; + private ShadowInfo mKeyShadowInfo; + private ShadowInfo mAmbientShadowInfo; + private int mDrawableSize; + private int mDrawableInsetSize; + private static final float KEY_SHADOW_ALPHA = 0.35f; + private static final float AMBIENT_SHADOW_ALPHA = 0.4f; public DreamOverlayStatusBarView(Context context) { this(context, null); @@ -73,6 +84,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); + mContext = context; } public DreamOverlayStatusBarView( @@ -80,14 +92,36 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } + @Override protected void onFinishInflate() { super.onFinishInflate(); + mKeyShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_key_text_shadow_radius, + R.dimen.dream_overlay_status_bar_key_text_shadow_dx, + R.dimen.dream_overlay_status_bar_key_text_shadow_dy, + KEY_SHADOW_ALPHA + ); + + mAmbientShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy, + AMBIENT_SHADOW_ALPHA + ); + + mDrawableSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size); + mDrawableInsetSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen); + mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE, - fetchStatusIconForResId(R.id.dream_overlay_wifi_status)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status))); mStatusIcons.put(STATUS_ICON_ALARM_SET, - fetchStatusIconForResId(R.id.dream_overlay_alarm_set)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set))); mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED, fetchStatusIconForResId(R.id.dream_overlay_camera_off)); mStatusIcons.put(STATUS_ICON_MIC_DISABLED, @@ -97,7 +131,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { mStatusIcons.put(STATUS_ICON_NOTIFICATIONS, fetchStatusIconForResId(R.id.dream_overlay_notification_indicator)); mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON, - fetchStatusIconForResId(R.id.dream_overlay_priority_mode)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode))); mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status); mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); @@ -137,4 +171,34 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { } return false; } + + private View addDoubleShadow(View icon) { + if (icon instanceof AlphaOptimizedImageView) { + AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon; + Drawable drawableIcon = i.getDrawable(); + i.setImageDrawable(new DoubleShadowIconDrawable( + mKeyShadowInfo, + mAmbientShadowInfo, + drawableIcon, + mDrawableSize, + mDrawableInsetSize + )); + } + return icon; + } + + private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) { + return new ShadowInfo( + fetchDimensionForResId(blurId), + fetchDimensionForResId(offsetXId), + fetchDimensionForResId(offsetYId), + alpha + ); + } + + private Float fetchDimensionForResId(int resId) { + return mContext + .getResources() + .getDimension(resId); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java index 92cdcf99f013..44207f4aecf5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java @@ -36,10 +36,10 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; -import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.wm.shell.animation.FlingAnimationUtils; @@ -274,16 +274,18 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { (float) Math.hypot(horizontalVelocity, verticalVelocity); final float expansion = flingRevealsOverlay(verticalVelocity, velocityVector) - ? KeyguardBouncer.EXPANSION_HIDDEN : KeyguardBouncer.EXPANSION_VISIBLE; + ? KeyguardBouncerConstants.EXPANSION_HIDDEN + : KeyguardBouncerConstants.EXPANSION_VISIBLE; // Log the swiping up to show Bouncer event. - if (!mBouncerInitiallyShowing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) { + if (!mBouncerInitiallyShowing + && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { mUiEventLogger.log(DreamEvent.DREAM_SWIPED); } flingToExpansion(verticalVelocity, expansion); - if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) { + if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { mStatusBarKeyguardViewManager.reset(false); } break; @@ -302,7 +304,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { float dragDownAmount = expansionFraction * expansionHeight; setPanelExpansion(expansionFraction, dragDownAmount); }); - if (!mBouncerInitiallyShowing && targetExpansion == KeyguardBouncer.EXPANSION_VISIBLE) { + if (!mBouncerInitiallyShowing + && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { animator.addListener( new AnimatorListenerAdapter() { @Override @@ -335,7 +338,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { final float targetHeight = viewHeight * expansion; final float expansionHeight = targetHeight - currentHeight; final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight); - if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) { + if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { // Hides the bouncer, i.e., fully expands the space above the bouncer. mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity, viewHeight); diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 20ae64cf5985..012615b0c653 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -75,15 +75,7 @@ object Flags { unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true) // TODO(b/254512731): Tracking Bug - @JvmField - val NOTIFICATION_DISMISSAL_FADE = - unreleasedFlag(113, "notification_dismissal_fade", teamfood = true) - - // TODO(b/259558771): Tracking Bug - val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix") - - // TODO(b/259559750): Tracking Bug - val SEMI_STABLE_SORT = releasedFlag(115, "semi_stable_sort") + @JvmField val NOTIFICATION_DISMISSAL_FADE = releasedFlag(113, "notification_dismissal_fade") @JvmField val USE_ROUNDNESS_SOURCETYPES = releasedFlag(116, "use_roundness_sourcetype") @@ -110,12 +102,14 @@ object Flags { val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD = unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true) + // TODO(b/263414400): Tracking Bug + @JvmField + val NOTIFICATION_ANIMATE_BIG_PICTURE = unreleasedFlag(120, "notification_animate_big_picture") + // 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, "lockscreen_animations") // TODO(b/254512750): Tracking Bug val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation") @@ -165,7 +159,7 @@ object Flags { // TODO(b/255618149): Tracking Bug @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = - unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false) + unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = true) /** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */ // TODO(b/256513609): Tracking Bug @@ -180,6 +174,13 @@ object Flags { @JvmField val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false) + /** + * Whether to use the new alternate bouncer architecture, a refactor of and eventual replacement + * of the Alternate/Authentication Bouncer. No visual UI changes. + */ + // TODO(b/260619425): Tracking Bug + @JvmField val MODERN_ALTERNATE_BOUNCER = releasedFlag(219, "modern_alternate_bouncer") + /** Flag to control the migration of face auth to modern architecture. */ // TODO(b/262838215): Tracking bug @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor") @@ -195,13 +196,22 @@ object Flags { /** A different path for unocclusion transitions back to keyguard */ // TODO(b/262859270): Tracking Bug @JvmField - val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false) + val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = true) // flag for controlling auto pin confirmation and material u shapes in bouncer @JvmField val AUTO_PIN_CONFIRMATION = unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") + // TODO(b/262859270): Tracking Bug + @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded") + + /** Enables code to show contextual loyalty cards in wallet entrypoints */ + // TODO(b/247587924): Tracking Bug + @JvmField + val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS = + unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -255,10 +265,11 @@ object Flags { // TODO(b/256614751): Tracking Bug val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = - unreleasedFlag(608, "new_status_bar_mobile_icons_backend") + unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true) // TODO(b/256613548): Tracking Bug - val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend") + val NEW_STATUS_BAR_WIFI_ICON_BACKEND = + unreleasedFlag(609, "new_status_bar_wifi_icon_backend", teamfood = true) // TODO(b/256623670): Tracking Bug @JvmField @@ -284,7 +295,7 @@ object Flags { // 801 - region sampling // TODO(b/254512848): Tracking Bug - val REGION_SAMPLING = unreleasedFlag(801, "region_sampling", teamfood = true) + val REGION_SAMPLING = unreleasedFlag(801, "region_sampling") // 803 - screen contents translation // TODO(b/254513187): Tracking Bug @@ -297,7 +308,7 @@ object Flags { // 900 - media // TODO(b/254512697): Tracking Bug - val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true) + val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer") // TODO(b/254512502): Tracking Bug val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions") @@ -327,13 +338,17 @@ object Flags { val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true) + // TODO(b/263512203): Tracking Bug + val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") - val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // TODO(b/265045965): Tracking Bug + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") // 1100 - windowing @Keep @@ -396,6 +411,17 @@ object Flags { val WM_DESKTOP_WINDOWING_2 = sysPropBooleanFlag(1112, "persist.wm.debug.desktop_mode_2", default = false) + // TODO(b/254513207): Tracking Bug to delete + @Keep + @JvmField + val WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES = + unreleasedFlag( + 1113, + name = "screen_record_enterprise_policies", + namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER, + teamfood = false + ) + // 1200 - predictive back @Keep @JvmField @@ -431,9 +457,6 @@ object Flags { unreleasedFlag(1206, "persist.wm.debug.predictive_back_bouncer_anim", teamfood = true) // 1300 - screenshots - // TODO(b/254512719): Tracking Bug - @JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor") - // TODO(b/254513155): Tracking Bug @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = @@ -462,8 +485,7 @@ object Flags { // 1800 - shade container @JvmField - val LEAVE_SHADE_OPEN_FOR_BUGREPORT = - unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true) + val LEAVE_SHADE_OPEN_FOR_BUGREPORT = releasedFlag(1800, "leave_shade_open_for_bugreport") // 1900 @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag") @@ -489,6 +511,7 @@ object Flags { @JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui") @JvmField val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications") + @JvmField val ENABLE_STYLUS_EDUCATION = unreleasedFlag(2303, "enable_stylus_education") // 2400 - performance tools and debugging info // TODO(b/238923086): Tracking Bug @@ -506,6 +529,16 @@ object Flags { @JvmField val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status") + // TODO(b/20911786): Tracking Bug + @JvmField + val OUTPUT_SWITCHER_SHOW_API_ENABLED = + unreleasedFlag(2503, "output_switcher_show_api_enabled", teamfood = true) + // TODO(b259590361): Tracking bug val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release") + + // 2600 - keyboard shortcut + // TODO(b/259352579): Tracking Bug + @JvmField + val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt index eaf1081a374a..482138e6c277 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt @@ -282,6 +282,7 @@ class CustomizationProvider : .ENABLEMENT_ACTION_TEXT, Contract.LockScreenQuickAffordances.AffordanceTable.Columns .ENABLEMENT_COMPONENT_NAME, + Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT, ) ) .apply { @@ -298,6 +299,7 @@ class CustomizationProvider : ), representation.actionText, representation.actionComponentName, + representation.configureIntent?.toUri(0), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index e644ed65da6c..0c003151878f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -34,6 +34,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE; import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS; +import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -122,6 +123,8 @@ import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -507,6 +510,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private CentralSurfaces mCentralSurfaces; + private boolean mUnocclusionTransitionFlagEnabled = false; + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = new DeviceConfig.OnPropertiesChangedListener() { @Override @@ -958,8 +963,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { - setOccluded(true /* isOccluded */, true /* animate */); - + if (!mUnocclusionTransitionFlagEnabled) { + setOccluded(true /* isOccluded */, true /* animate */); + } if (apps == null || apps.length == 0 || apps[0] == null) { if (DEBUG) { Log.d(TAG, "No apps provided to the OccludeByDream runner; " @@ -1001,9 +1007,20 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, applier.scheduleApply(paramsBuilder.build()); }); mOccludeByDreamAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mIsCancelled = false; + @Override + public void onAnimationCancel(Animator animation) { + mIsCancelled = true; + } + @Override public void onAnimationEnd(Animator animation) { try { + if (!mIsCancelled && mUnocclusionTransitionFlagEnabled) { + // We're already on the main thread, don't queue this call + handleSetOccluded(true /* isOccluded */, + false /* animate */); + } finishedCallback.onAnimationFinished(); mOccludeByDreamAnimator = null; } catch (RemoteException e) { @@ -1176,6 +1193,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, ScreenOnCoordinator screenOnCoordinator, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, + FeatureFlags featureFlags, Lazy<ShadeController> shadeControllerLazy, Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy, Lazy<ActivityLaunchAnimator> activityLaunchAnimator, @@ -1230,9 +1248,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, R.dimen.physical_power_button_center_screen_location_y); mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); - mDreamOpenAnimationDuration = context.getResources().getInteger( - com.android.internal.R.integer.config_dreamOpenAnimationDuration); + mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS; mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS; + mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION); } public void userActivity() { @@ -1792,7 +1810,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.beginSection("KeyguardViewMediator#setOccluded"); if (DEBUG) Log.d(TAG, "setOccluded " + isOccluded); - mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD); mHandler.removeMessages(SET_OCCLUDED); Message msg = mHandler.obtainMessage(SET_OCCLUDED, isOccluded ? 1 : 0, animate ? 1 : 0); mHandler.sendMessage(msg); @@ -1825,6 +1842,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private void handleSetOccluded(boolean isOccluded, boolean animate) { Trace.beginSection("KeyguardViewMediator#handleSetOccluded"); Log.d(TAG, "handleSetOccluded(" + isOccluded + ")"); + mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD); + synchronized (KeyguardViewMediator.this) { if (mHiding && isOccluded) { // We're in the process of going away but WindowManager wants to show a @@ -1893,12 +1912,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * Enable the keyguard if the settings are appropriate. */ private void doKeyguardLocked(Bundle options) { - if (KeyguardUpdateMonitor.CORE_APPS_ONLY) { - // Don't show keyguard during half-booted cryptkeeper stage. - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because booting to cryptkeeper"); - return; - } - // if another app is disabling us, don't show if (!mExternallyEnabled) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); @@ -1907,13 +1920,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } - // if the keyguard is already showing, don't bother. check flags in both files - // to account for the hiding animation which results in a delay and discrepancy - // between flags + // If the keyguard is already showing, see if we don't need to bother re-showing it. Check + // flags in both files to account for the hiding animation which results in a delay and + // discrepancy between flags. if (mShowing && mKeyguardStateController.isShowing()) { - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); - resetStateLocked(); - return; + if (mPM.isInteractive()) { + // It's already showing, and we're not trying to show it while the screen is off. + // We can simply reset all of the views. + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + resetStateLocked(); + return; + } else { + // We are trying to show the keyguard while the screen is off - this results from + // race conditions involving locking while unlocking. Don't short-circuit here and + // ensure the keyguard is fully re-shown. + Log.e(TAG, + "doKeyguard: already showing, but re-showing since we're not interactive"); + } } // In split system user mode, we never unlock system user. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java index 017b65acd1d2..ffd8a0244a86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java @@ -33,6 +33,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -63,6 +64,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private final Context mContext; private final DisplayMetrics mDisplayMetrics; + private final SystemClock mSystemClock; @Nullable private final IWallpaperManager mWallpaperManagerService; @@ -71,6 +73,9 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN; + public static final long UNKNOWN_LAST_WAKE_TIME = -1; + private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME; + @Nullable private Point mLastWakeOriginLocation = null; @@ -84,10 +89,12 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe public WakefulnessLifecycle( Context context, @Nullable IWallpaperManager wallpaperManagerService, + SystemClock systemClock, DumpManager dumpManager) { mContext = context; mDisplayMetrics = context.getResources().getDisplayMetrics(); mWallpaperManagerService = wallpaperManagerService; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getSimpleName(), this); } @@ -104,6 +111,14 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } /** + * Returns the most recent time (in device uptimeMillis) the display woke up. + * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet. + */ + public long getLastWakeTime() { + return mLastWakeTime; + } + + /** * Returns the most recent reason the device went to sleep up. This is one of * PowerManager.GO_TO_SLEEP_REASON_*. */ @@ -117,6 +132,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } setWakefulness(WAKEFULNESS_WAKING); mLastWakeReason = pmWakeReason; + mLastWakeTime = mSystemClock.uptimeMillis(); updateLastWakeOriginLocation(); if (mWallpaperManagerService != null) { 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 47ef0fac17ab..98d3570106ce 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -39,6 +39,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -112,6 +113,7 @@ public class KeyguardModule { ScreenOnCoordinator screenOnCoordinator, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, + FeatureFlags featureFlags, Lazy<ShadeController> shadeController, Lazy<NotificationShadeWindowController> notificationShadeWindowController, Lazy<ActivityLaunchAnimator> activityLaunchAnimator, @@ -142,6 +144,7 @@ public class KeyguardModule { screenOnCoordinator, interactionJankMonitor, dreamOverlayStateController, + featureFlags, shadeController, notificationShadeWindowController, activityLaunchAnimator, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt index 80c6130955c5..faeb48526ae4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data import android.view.KeyEvent +import android.window.OnBackAnimationCallback import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import java.lang.ref.WeakReference @@ -51,4 +52,6 @@ interface BouncerViewDelegate { cancelAction: Runnable?, ) fun willDismissWithActions(): Boolean + /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */ + fun getBackCallback(): OnBackAnimationCallback } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index ea5b4f43cc75..cd4dac0d59c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -30,5 +30,6 @@ object BuiltInKeyguardQuickAffordanceKeys { const val HOME_CONTROLS = "home" const val QR_CODE_SCANNER = "qr_code_scanner" const val QUICK_ACCESS_WALLET = "wallet" + const val VIDEO_CAMERA = "video_camera" // Please keep alphabetical order of const names to simplify future maintenance. } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index dbc376e62950..f6e6d6b7dc1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -26,6 +26,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.StatusBarState import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -46,7 +47,7 @@ constructor( get() = context.getString(R.string.accessibility_camera_button) override val pickerIconResourceId: Int - get() = com.android.internal.R.drawable.perm_group_camera + get() = R.drawable.ic_camera override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> get() = @@ -54,12 +55,20 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = Icon.Resource( - com.android.internal.R.drawable.perm_group_camera, + R.drawable.ic_camera, ContentDescription.Resource(R.string.accessibility_camera_button) ) ) ) + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { + return if (isLaunchable()) { + super.getPickerScreenState() + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } + } + override fun onTriggered( expandable: Expandable? ): KeyguardQuickAffordanceConfig.OnTriggeredResult { @@ -68,4 +77,8 @@ constructor( .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } + + private fun isLaunchable(): Boolean { + return cameraGestureHelper.get().canCameraGestureBeLaunched(StatusBarState.KEYGUARD) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt index 8efb36624831..ed1ff329004a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context +import android.content.Intent import android.net.Uri import android.provider.Settings import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS @@ -39,6 +40,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -48,10 +50,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import javax.inject.Inject @SysUISingleton -class DoNotDisturbQuickAffordanceConfig constructor( +class DoNotDisturbQuickAffordanceConfig +constructor( private val context: Context, private val controller: ZenModeController, private val secureSettings: SecureSettings, @@ -59,7 +61,7 @@ class DoNotDisturbQuickAffordanceConfig constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val testConditionId: Uri?, testDialog: EnableZenModeDialog?, -): KeyguardQuickAffordanceConfig { +) : KeyguardQuickAffordanceConfig { @Inject constructor( @@ -76,20 +78,23 @@ class DoNotDisturbQuickAffordanceConfig constructor( private val conditionUri: Uri get() = - testConditionId ?: ZenModeConfig.toTimeCondition( - context, - settingsValue, - userTracker.userId, - true, /* shortVersion */ - ).id + testConditionId + ?: ZenModeConfig.toTimeCondition( + context, + settingsValue, + userTracker.userId, + true, /* shortVersion */ + ) + .id private val dialog: EnableZenModeDialog by lazy { - testDialog ?: EnableZenModeDialog( - context, - R.style.Theme_SystemUI_Dialog, - true, /* cancelIsNeutral */ - ZenModeDialogMetricsLogger(context), - ) + testDialog + ?: EnableZenModeDialog( + context, + R.style.Theme_SystemUI_Dialog, + true, /* cancelIsNeutral */ + ZenModeDialogMetricsLogger(context), + ) } override val key: String = BuiltInKeyguardQuickAffordanceKeys.DO_NOT_DISTURB @@ -98,58 +103,62 @@ class DoNotDisturbQuickAffordanceConfig constructor( override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb - override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = combine( - conflatedCallbackFlow { - val callback = object: ZenModeController.Callback { - override fun onZenChanged(zen: Int) { - dndMode = zen - trySendWithFailureLogging(updateState(), TAG) - } - - override fun onZenAvailableChanged(available: Boolean) { - isAvailable = available - trySendWithFailureLogging(updateState(), TAG) - } - } - - dndMode = controller.zen - isAvailable = controller.isZenAvailable - trySendWithFailureLogging(updateState(), TAG) - - controller.addCallback(callback) - - awaitClose { controller.removeCallback(callback) } - }, - secureSettings - .observerFlow(Settings.Secure.ZEN_DURATION) - .onStart { emit(Unit) } - .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) } - .flowOn(backgroundDispatcher) - .distinctUntilChanged() - .onEach { settingsValue = it } - ) { callbackFlowValue, _ -> callbackFlowValue } + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = + combine( + conflatedCallbackFlow { + val callback = + object : ZenModeController.Callback { + override fun onZenChanged(zen: Int) { + dndMode = zen + trySendWithFailureLogging(updateState(), TAG) + } + + override fun onZenAvailableChanged(available: Boolean) { + isAvailable = available + trySendWithFailureLogging(updateState(), TAG) + } + } + + dndMode = controller.zen + isAvailable = controller.isZenAvailable + trySendWithFailureLogging(updateState(), TAG) + + controller.addCallback(callback) + + awaitClose { controller.removeCallback(callback) } + }, + secureSettings + .observerFlow(Settings.Secure.ZEN_DURATION) + .onStart { emit(Unit) } + .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + .onEach { settingsValue = it } + ) { callbackFlowValue, _ -> callbackFlowValue } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { return if (controller.isZenAvailable) { - KeyguardQuickAffordanceConfig.PickerScreenState.Default + KeyguardQuickAffordanceConfig.PickerScreenState.Default( + configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) + ) } else { KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice } } - override fun onTriggered(expandable: Expandable?): - KeyguardQuickAffordanceConfig.OnTriggeredResult { + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { return when { - !isAvailable -> - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled dndMode != ZEN_MODE_OFF -> { controller.setZen(ZEN_MODE_OFF, null, TAG) KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } settingsValue == ZEN_DURATION_PROMPT -> KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog( - dialog.createDialog(), - expandable + dialog.createDialog(), + expandable ) settingsValue == ZEN_DURATION_FOREVER -> { controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG) @@ -187,4 +196,4 @@ class DoNotDisturbQuickAffordanceConfig constructor( companion object { const val TAG = "DoNotDisturbQuickAffordanceConfig" } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 62fe80a82908..3412f35669e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -135,7 +135,7 @@ constructor( override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = if (flashlightController.isAvailable) { - KeyguardQuickAffordanceConfig.PickerScreenState.Default + KeyguardQuickAffordanceConfig.PickerScreenState.Default() } else { KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index 09e5ec0065f8..a1e9137d1764 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -90,7 +90,7 @@ constructor( ) } - return KeyguardQuickAffordanceConfig.PickerScreenState.Default + return KeyguardQuickAffordanceConfig.PickerScreenState.Default() } override fun onTriggered( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index 71d01ebc8496..a1cce5c670ba 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -39,6 +39,7 @@ interface KeyguardDataQuickAffordanceModule { quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, camera: CameraQuickAffordanceConfig, + videoCamera: VideoCameraQuickAffordanceConfig, ): Set<KeyguardQuickAffordanceConfig> { return setOf( camera, @@ -47,6 +48,7 @@ interface KeyguardDataQuickAffordanceModule { home, quickAccessWallet, qrCodeScanner, + videoCamera, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 20588e9ccdc1..e32edcb010e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -46,7 +46,7 @@ interface KeyguardQuickAffordanceConfig { * Returns the [PickerScreenState] representing the affordance in the settings or selector * experience. */ - suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default + suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default() /** * Notifies that the affordance was clicked by the user. @@ -63,7 +63,10 @@ interface KeyguardQuickAffordanceConfig { sealed class PickerScreenState { /** The picker shows the item for selecting this affordance as it normally would. */ - object Default : PickerScreenState() + data class Default( + /** Optional [Intent] to use to start an activity to configure this affordance. */ + val configureIntent: Intent? = null, + ) : PickerScreenState() /** * The picker does not show an item for selecting this affordance as it is not supported on diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index 4f7990ff0deb..ea6c107cd161 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -89,7 +89,7 @@ constructor( ), ), ) - else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default + else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index 1928f40fa059..680c06bf2c64 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -128,7 +128,7 @@ constructor( actionComponentName = componentName, ) } - else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default + else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt new file mode 100644 index 000000000000..d9ec3b1c2f87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.app.StatusBarManager +import android.content.Context +import android.content.Intent +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.camera.CameraIntents +import com.android.systemui.camera.CameraIntentsWrapper +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +@SysUISingleton +class VideoCameraQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val cameraIntents: CameraIntentsWrapper, + private val activityIntentHelper: ActivityIntentHelper, + private val userTracker: UserTracker, +) : KeyguardQuickAffordanceConfig { + + private val intent: Intent by lazy { + cameraIntents.getVideoCameraIntent().apply { + putExtra( + CameraIntents.EXTRA_LAUNCH_SOURCE, + StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE, + ) + } + } + + override val key: String + get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA + + override val pickerName: String + get() = context.getString(R.string.video_camera) + + override val pickerIconResourceId: Int + get() = R.drawable.ic_videocam + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> + get() = + flowOf( + if (isLaunchable()) { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( + R.drawable.ic_videocam, + ContentDescription.Resource(R.string.video_camera) + ) + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } + ) + + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { + return if (isLaunchable()) { + super.getPickerScreenState() + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } + } + + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = intent, + canShowWhileLocked = false, + ) + } + + private fun isLaunchable(): Boolean { + return activityIntentHelper.getTargetActivityInfo( + intent, + userTracker.userId, + true, + ) != null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt new file mode 100644 index 000000000000..25d8f4021f87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt @@ -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 com.android.systemui.keyguard.data.repository + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED +import android.content.Context +import android.content.IntentFilter +import android.os.Looper +import android.os.UserHandle +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.biometrics.AuthController +import com.android.systemui.broadcast.BroadcastDispatcher +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.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +/** + * Acts as source of truth for biometric features. + * + * Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about + * upstream changes. + */ +interface BiometricRepository { + /** Whether any fingerprints are enrolled for the current user. */ + val isFingerprintEnrolled: StateFlow<Boolean> + + /** + * Whether the current user is allowed to use a strong biometric for device entry based on + * Android Security policies. If false, the user may be able to use primary authentication for + * device entry. + */ + val isStrongBiometricAllowed: StateFlow<Boolean> + + /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */ + val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> +} + +@SysUISingleton +class BiometricRepositoryImpl +@Inject +constructor( + context: Context, + lockPatternUtils: LockPatternUtils, + broadcastDispatcher: BroadcastDispatcher, + authController: AuthController, + userRepository: UserRepository, + devicePolicyManager: DevicePolicyManager, + @Application scope: CoroutineScope, + @Background backgroundDispatcher: CoroutineDispatcher, + @Main looper: Looper, +) : BiometricRepository { + + /** UserId of the current selected user. */ + private val selectedUserId: Flow<Int> = + userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged() + + override val isFingerprintEnrolled: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { userId -> + conflatedCallbackFlow { + val callback = + object : AuthController.Callback { + override fun onEnrollmentsChanged( + sensorBiometricType: BiometricType, + userId: Int, + hasEnrollments: Boolean + ) { + if (sensorBiometricType.isFingerprint) { + trySendWithFailureLogging( + hasEnrollments, + TAG, + "update fpEnrollment" + ) + } + } + } + authController.addCallback(callback) + awaitClose { authController.removeCallback(callback) } + } + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id) + ) + + override val isStrongBiometricAllowed: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { currUserId -> + conflatedCallbackFlow { + val callback = + object : LockPatternUtils.StrongAuthTracker(context, looper) { + override fun onStrongAuthRequiredChanged(userId: Int) { + if (currUserId != userId) { + return + } + + trySendWithFailureLogging( + isBiometricAllowedForUser(true, currUserId), + TAG + ) + } + + override fun onIsNonStrongBiometricAllowedChanged(userId: Int) { + // no-op + } + } + lockPatternUtils.registerStrongAuthTracker(callback) + awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) } + } + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + lockPatternUtils.isBiometricAllowedForUser( + userRepository.getSelectedUserInfo().id + ) + ) + + override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { userId -> + broadcastDispatcher + .broadcastFlow( + filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + user = UserHandle.ALL + ) + .transformLatest { + emit( + (devicePolicyManager.getKeyguardDisabledFeatures(null, userId) and + DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) == 0 + ) + } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + devicePolicyManager.getKeyguardDisabledFeatures( + null, + userRepository.getSelectedUserInfo().id + ) and DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT == 0 + ) + + companion object { + private const val TAG = "BiometricsRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt new file mode 100644 index 000000000000..93c97813d8e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt @@ -0,0 +1,33 @@ +/* + * 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 + +enum class BiometricType(val isFingerprint: Boolean) { + // An unsupported biometric type + UNKNOWN(false), + + // Fingerprint sensor that is located on the back (opposite side of the display) of the device + REAR_FINGERPRINT(true), + + // Fingerprint sensor that is located under the display + UNDER_DISPLAY_FINGERPRINT(true), + + // Fingerprint sensor that is located on the side of the device, typically on the power button + SIDE_FINGERPRINT(true), + FACE(false), +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt index 90f3c7d88c8f..41574d18e2c7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt @@ -20,15 +20,18 @@ import android.os.Build import com.android.keyguard.ViewMediatorCallback import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.log.dagger.BouncerLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.phone.KeyguardBouncer +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn @@ -44,6 +47,7 @@ class KeyguardBouncerRepository @Inject constructor( private val viewMediatorCallback: ViewMediatorCallback, + private val clock: SystemClock, @Application private val applicationScope: CoroutineScope, @BouncerLog private val buffer: TableLogBuffer, ) { @@ -70,7 +74,7 @@ constructor( * 1f = panel fully showing = bouncer fully hidden * ``` */ - private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN) + private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN) val panelExpansionAmount = _panelExpansionAmount.asStateFlow() private val _keyguardPosition = MutableStateFlow(0f) val keyguardPosition = _keyguardPosition.asStateFlow() @@ -94,6 +98,14 @@ constructor( setUpLogging() } + /** Values associated with the AlternateBouncer */ + private val _isAlternateBouncerVisible = MutableStateFlow(false) + val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow() + var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE + private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false) + val isAlternateBouncerUIAvailable: StateFlow<Boolean> = + _isAlternateBouncerUIAvailable.asStateFlow() + fun setPrimaryScrimmed(isScrimmed: Boolean) { _primaryBouncerScrimmed.value = isScrimmed } @@ -102,6 +114,19 @@ constructor( _primaryBouncerVisible.value = isVisible } + fun setAlternateVisible(isVisible: Boolean) { + if (isVisible && !_isAlternateBouncerVisible.value) { + lastAlternateBouncerVisibleTime = clock.uptimeMillis() + } else if (!isVisible) { + lastAlternateBouncerVisibleTime = NOT_VISIBLE + } + _isAlternateBouncerVisible.value = isVisible + } + + fun setAlternateBouncerUIAvailable(isAvailable: Boolean) { + _isAlternateBouncerUIAvailable.value = isAvailable + } + fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) { _primaryBouncerShow.value = keyguardBouncerModel } @@ -202,4 +227,8 @@ constructor( .logDiffsForTable(buffer, "", "ResourceUpdateRequests", false) .launchIn(applicationScope) } + + companion object { + private const val NOT_VISIBLE = -1L + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index e3f5e90b2300..2b2b9d0703fa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -187,6 +187,8 @@ constructor( pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice } .map { (config, pickerState) -> + val defaultPickerState = + pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Default val disabledPickerState = pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled KeyguardQuickAffordancePickerRepresentation( @@ -198,6 +200,7 @@ constructor( instructions = disabledPickerState?.instructions, actionText = disabledPickerState?.actionText, actionComponentName = disabledPickerState?.actionComponentName, + configureIntent = defaultPickerState?.configureIntent, ) } } 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 a4fd087a24b1..d99af90ab6dc 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 @@ -40,6 +40,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -88,6 +89,9 @@ interface KeyguardRepository { /** Observable for whether the bouncer is showing. */ val isBouncerShowing: Flow<Boolean> + /** Is the always-on display available to be used? */ + val isAodAvailable: Flow<Boolean> + /** * Observable for whether we are in doze state. * @@ -182,6 +186,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, + private val dozeParameters: DozeParameters, private val authController: AuthController, private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { @@ -220,6 +225,31 @@ constructor( } .distinctUntilChanged() + override val isAodAvailable: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DozeParameters.Callback { + override fun onAlwaysOnChange() { + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "updated isAodAvailable" + ) + } + } + + dozeParameters.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "initial isAodAvailable" + ) + + awaitClose { dozeParameters.removeCallback(callback) } + } + .distinctUntilChanged() + override val isKeyguardOccluded: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index 26f853f3ad1c..4639597a9b8c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -30,4 +30,6 @@ interface KeyguardRepositoryModule { @Binds fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository + + @Binds fun biometricRepository(impl: BiometricRepositoryImpl): BiometricRepository } 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 index 343c2dc172fc..0c4bca616e12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -135,11 +135,14 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") return null } - if (lastStep.transitionState != TransitionState.FINISHED) { - Log.i(TAG, "Transition still active: $lastStep, canceling") - } + val startingValue = + if (lastStep.transitionState != TransitionState.FINISHED) { + Log.i(TAG, "Transition still active: $lastStep, canceling") + lastStep.value + } else { + 0f + } - val startingValue = 1f - lastStep.value lastAnimator?.cancel() lastAnimator = info.animator @@ -206,7 +209,7 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio return } - if (state == TransitionState.FINISHED) { + if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { updateTransitionId = null } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt new file mode 100644 index 000000000000..d90f328719bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.app.trust.TrustManager +import com.android.keyguard.logging.TrustRepositoryLogger +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.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.TrustModel +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn + +/** Encapsulates any state relevant to trust agents and trust grants. */ +interface TrustRepository { + /** Flow representing whether the current user is trusted. */ + val isCurrentUserTrusted: Flow<Boolean> +} + +@SysUISingleton +class TrustRepositoryImpl +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val userRepository: UserRepository, + private val trustManager: TrustManager, + private val logger: TrustRepositoryLogger, +) : TrustRepository { + private val latestTrustModelForUser = mutableMapOf<Int, TrustModel>() + + private val trust = + conflatedCallbackFlow { + val callback = + object : TrustManager.TrustListener { + override fun onTrustChanged( + enabled: Boolean, + newlyUnlocked: Boolean, + userId: Int, + flags: Int, + grantMsgs: List<String>? + ) { + logger.onTrustChanged(enabled, newlyUnlocked, userId, flags, grantMsgs) + trySendWithFailureLogging( + TrustModel(enabled, userId), + TrustRepositoryLogger.TAG, + "onTrustChanged" + ) + } + + override fun onTrustError(message: CharSequence?) = Unit + + override fun onTrustManagedChanged(enabled: Boolean, userId: Int) = Unit + } + trustManager.registerTrustListener(callback) + logger.trustListenerRegistered() + awaitClose { + logger.trustListenerUnregistered() + trustManager.unregisterTrustListener(callback) + } + } + .onEach { + latestTrustModelForUser[it.userId] = it + logger.trustModelEmitted(it) + } + .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1) + + override val isCurrentUserTrusted: Flow<Boolean> + get() = + combine(trust, userRepository.selectedUserInfo, ::Pair) + .map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false } + .distinctUntilChanged() + .onEach { logger.isCurrentUserTrusted(it) } + .onStart { emit(false) } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt new file mode 100644 index 000000000000..28c0b288147b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt @@ -0,0 +1,126 @@ +/* + * 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.keyguard.KeyguardUpdateMonitor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.BiometricRepository +import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */ +@SysUISingleton +class AlternateBouncerInteractor +@Inject +constructor( + private val bouncerRepository: KeyguardBouncerRepository, + private val biometricRepository: BiometricRepository, + private val systemClock: SystemClock, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + featureFlags: FeatureFlags, +) { + val isModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER) + var legacyAlternateBouncer: LegacyAlternateBouncer? = null + var legacyAlternateBouncerVisibleTime: Long = NOT_VISIBLE + + val isVisible: Flow<Boolean> = bouncerRepository.isAlternateBouncerVisible + + /** + * Sets the correct bouncer states to show the alternate bouncer if it can show. + * @return whether alternateBouncer is visible + */ + fun show(): Boolean { + return when { + isModernAlternateBouncerEnabled -> { + bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint()) + isVisibleState() + } + canShowAlternateBouncerForFingerprint() -> { + if (legacyAlternateBouncer?.showAlternateBouncer() == true) { + legacyAlternateBouncerVisibleTime = systemClock.uptimeMillis() + true + } else { + false + } + } + else -> false + } + } + + /** + * Sets the correct bouncer states to hide the bouncer. Should only be called through + * StatusBarKeyguardViewManager until ScrimController is refactored to use + * alternateBouncerInteractor. + * @return true if the alternate bouncer was newly hidden, else false. + */ + fun hide(): Boolean { + return if (isModernAlternateBouncerEnabled) { + val wasAlternateBouncerVisible = isVisibleState() + bouncerRepository.setAlternateVisible(false) + wasAlternateBouncerVisible && !isVisibleState() + } else { + legacyAlternateBouncer?.hideAlternateBouncer() ?: false + } + } + + fun isVisibleState(): Boolean { + return if (isModernAlternateBouncerEnabled) { + bouncerRepository.isAlternateBouncerVisible.value + } else { + legacyAlternateBouncer?.isShowingAlternateBouncer ?: false + } + } + + fun setAlternateBouncerUIAvailable(isAvailable: Boolean) { + bouncerRepository.setAlternateBouncerUIAvailable(isAvailable) + } + + fun canShowAlternateBouncerForFingerprint(): Boolean { + return if (isModernAlternateBouncerEnabled) { + bouncerRepository.isAlternateBouncerUIAvailable.value && + biometricRepository.isFingerprintEnrolled.value && + biometricRepository.isStrongBiometricAllowed.value && + biometricRepository.isFingerprintEnabledByDevicePolicy.value + } else { + legacyAlternateBouncer != null && + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true) + } + } + + /** + * Whether the alt bouncer has shown for a minimum time before allowing touches to dismiss the + * alternate bouncer and show the primary bouncer. + */ + fun hasAlternateBouncerShownWithMinTime(): Boolean { + return if (isModernAlternateBouncerEnabled) { + (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) > + MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS + } else { + systemClock.uptimeMillis() - legacyAlternateBouncerVisibleTime > 200 + } + } + + companion object { + private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L + private const val NOT_VISIBLE = -1L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index fd2d271e40f9..ce61f2fec92f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,9 +21,9 @@ 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.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration @@ -48,12 +48,11 @@ constructor( private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.dozeTransitionModel + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeTransitionModel, lastStartedTransition) = pair + .collect { (wakefulnessModel, lastStartedTransition) -> if ( - isDozeOff(dozeTransitionModel.to) && + isWakingOrStartingToWake(wakefulnessModel) && lastStartedTransition.to == KeyguardState.DOZING ) { keyguardTransitionRepository.startTransition( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3b09ae7ba8ea..81a58286aab7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -21,7 +21,7 @@ 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.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState @@ -31,8 +31,10 @@ import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @SysUISingleton @@ -56,7 +58,7 @@ constructor( scope.launch { // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which // otherwise would have gone through OCCLUDED first - keyguardInteractor.isDreamingWithOverlay + keyguardInteractor.isAbleToDream .sample( combine( keyguardInteractor.dozeTransitionModel, @@ -65,8 +67,7 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple + .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) -> if ( !isDreaming && isDozeOff(dozeTransitionModel.to) && @@ -88,6 +89,9 @@ constructor( private fun listenForDreamingToOccluded() { scope.launch { keyguardInteractor.isDreaming + // Add a slight delay, as dreaming and occluded events will arrive with a small gap + // in time. This prevents a transition to OCCLUSION happening prematurely. + .onEach { delay(50) } .sample( combine( keyguardInteractor.isKeyguardOccluded, @@ -96,8 +100,7 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, isOccluded, lastStartedTransition) = triple + .collect { (isDreaming, isOccluded, lastStartedTransition) -> if ( isOccluded && !isDreaming && @@ -123,24 +126,18 @@ constructor( private fun listenForDreamingToGone() { scope.launch { - keyguardInteractor.biometricUnlockState - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (biometricUnlockState, keyguardState) = pair - if ( - keyguardState == KeyguardState.DREAMING && - isWakeAndUnlock(biometricUnlockState) - ) { - keyguardTransitionRepository.startTransition( - TransitionInfo( - name, - KeyguardState.DREAMING, - KeyguardState.GONE, - getAnimator(), - ) + keyguardInteractor.biometricUnlockState.collect { biometricUnlockState -> + if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.DREAMING, + KeyguardState.GONE, + getAnimator(), ) - } + ) } + } } } @@ -151,8 +148,7 @@ constructor( keyguardTransitionInteractor.finishedKeyguardState, ::Pair ) - .collect { pair -> - val (dozeTransitionModel, keyguardState) = pair + .collect { (dozeTransitionModel, keyguardState) -> if ( dozeTransitionModel.to == DozeStateModel.DOZE && keyguardState == KeyguardState.DREAMING diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 553fafeb92c3..14f918d78bc6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -26,7 +26,10 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -40,23 +43,22 @@ constructor( ) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { - listenForGoneToAod() + listenForGoneToAodOrDozing() listenForGoneToDreaming() } private fun listenForGoneToDreaming() { scope.launch { keyguardInteractor.isAbleToDream - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (isAbleToDream, keyguardState) = pair - if (isAbleToDream && keyguardState == KeyguardState.GONE) { + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { (isAbleToDream, lastStartedStep) -> + if (isAbleToDream && lastStartedStep.to == KeyguardState.GONE) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.GONE, KeyguardState.DREAMING, - getAnimator(), + getAnimator(TO_DREAMING_DURATION), ) ) } @@ -64,21 +66,31 @@ constructor( } } - private fun listenForGoneToAod() { + private fun listenForGoneToAodOrDozing() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (wakefulnessState, keyguardState) = pair + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> if ( - keyguardState == KeyguardState.GONE && + lastStartedStep.to == KeyguardState.GONE && wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.GONE, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) @@ -87,14 +99,15 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 326acc9eb762..5674e2a15271 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,15 +21,17 @@ 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.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine @@ -46,12 +48,11 @@ constructor( private val keyguardTransitionRepository: KeyguardTransitionRepository, ) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) { - private var transitionId: UUID? = null - override fun start() { listenForLockscreenToGone() listenForLockscreenToOccluded() - listenForLockscreenToAod() + listenForLockscreenToCamera() + listenForLockscreenToAodOrDozing() listenForLockscreenToBouncer() listenForLockscreenToDreaming() listenForLockscreenToBouncerDragging() @@ -69,7 +70,7 @@ constructor( name, KeyguardState.LOCKSCREEN, KeyguardState.DREAMING, - getAnimator(), + getAnimator(TO_DREAMING_DURATION), ) ) } @@ -101,6 +102,7 @@ constructor( /* Starts transitions when manually dragging up the bouncer from the lockscreen. */ private fun listenForLockscreenToBouncerDragging() { + var transitionId: UUID? = null scope.launch { shadeRepository.shadeModel .sample( @@ -111,25 +113,43 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (shadeModel, keyguardState, statusBarState) = triple - + .collect { (shadeModel, keyguardState, statusBarState) -> 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, - 1f - shadeModel.expansionAmount, - if ( - shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f - ) { - transitionId = null + // `updateTransition` will control it until FINISHED or CANCELED + var nextState = + if (shadeModel.expansionAmount == 0f) { TransitionState.FINISHED + } else if (shadeModel.expansionAmount == 1f) { + TransitionState.CANCELED } else { TransitionState.RUNNING } + keyguardTransitionRepository.updateTransition( + id, + 1f - shadeModel.expansionAmount, + nextState, ) + + if ( + nextState == TransitionState.CANCELED || + nextState == TransitionState.FINISHED + ) { + transitionId = null + } + + // If canceled, just put the state back + if (nextState == TransitionState.CANCELED) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = KeyguardState.LOCKSCREEN, + animator = getAnimator(0.milliseconds) + ) + ) + } } else { // TODO (b/251849525): Remove statusbarstate check when that state is // integrated into KeyguardTransitionRepository @@ -184,17 +204,14 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isOccluded, keyguardState, isDreaming) = triple - // Occlusion signals come from the framework, and should interrupt any - // existing transition - if (isOccluded && !isDreaming) { + .collect { (isOccluded, keyguardState, isDreaming) -> + if (isOccluded && !isDreaming && keyguardState == KeyguardState.LOCKSCREEN) { keyguardTransitionRepository.startTransition( TransitionInfo( name, keyguardState, KeyguardState.OCCLUDED, - getAnimator(), + getAnimator(TO_OCCLUDED_DURATION), ) ) } @@ -202,19 +219,59 @@ constructor( } } - private fun listenForLockscreenToAod() { + /** This signal may come in before the occlusion signal, and can provide a custom transition */ + private fun listenForLockscreenToCamera() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) + keyguardInteractor.onCameraLaunchDetected .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + .collect { (_, lastStartedStep) -> + // DREAMING/AOD/OFF may trigger on the first power button push, so include this + // state in order to cancel and correct the transition + if ( + lastStartedStep.to == KeyguardState.LOCKSCREEN || + lastStartedStep.to == KeyguardState.DREAMING || + lastStartedStep.to == KeyguardState.DOZING || + lastStartedStep.to == KeyguardState.AOD || + lastStartedStep.to == KeyguardState.OFF + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.OCCLUDED, + getAnimator(TO_OCCLUDED_DURATION), + ) + ) + } + } + } + } + + private fun listenForLockscreenToAodOrDozing() { + scope.launch { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.LOCKSCREEN && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.LOCKSCREEN, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) @@ -223,14 +280,16 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds + val TO_OCCLUDED_DURATION = 450.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 88789019b10f..2dc8fee25379 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -23,12 +23,14 @@ import com.android.systemui.dagger.qualifiers.Application 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 com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -44,6 +46,7 @@ constructor( override fun start() { listenForOccludedToLockscreen() listenForOccludedToDreaming() + listenForOccludedToAodOrDozing() } private fun listenForOccludedToDreaming() { @@ -70,8 +73,7 @@ constructor( scope.launch { keyguardInteractor.isKeyguardOccluded .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (isOccluded, lastStartedKeyguardState) = pair + .collect { (isOccluded, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any // existing transition if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { @@ -88,6 +90,39 @@ constructor( } } + private fun listenForOccludedToAodOrDozing() { + scope.launch { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.OCCLUDED && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) 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 402c1793f0b2..4cf56fe2c031 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 @@ -17,20 +17,30 @@ package com.android.systemui.keyguard.domain.interactor +import android.app.StatusBarManager import android.graphics.Point +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.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel -import com.android.systemui.util.kotlin.sample +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.CommandQueue.Callbacks import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.merge /** @@ -41,6 +51,7 @@ class KeyguardInteractor @Inject constructor( private val repository: KeyguardRepository, + private val commandQueue: CommandQueue, ) { /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at @@ -49,6 +60,8 @@ constructor( val dozeAmount: Flow<Float> = repository.linearDozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing + /** Whether Always-on Display mode is available. */ + val isAodAvailable: Flow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel /** @@ -58,19 +71,44 @@ constructor( val isDreaming: Flow<Boolean> = repository.isDreaming /** Whether the system is dreaming with an overlay active */ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay + /** Event for when the camera gesture is detected */ + val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = conflatedCallbackFlow { + val callback = + object : CommandQueue.Callbacks { + override fun onCameraLaunchGestureDetected(source: Int) { + trySendWithFailureLogging( + cameraLaunchSourceIntToModel(source), + TAG, + "updated onCameraLaunchGestureDetected" + ) + } + } + + commandQueue.addCallback(callback) + + awaitClose { commandQueue.removeCallback(callback) } + } /** * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means * that doze mode is not running and DREAMING is ok to commence. + * + * Allow a brief moment to prevent rapidly oscillating between true/false signals. */ val isAbleToDream: Flow<Boolean> = merge(isDreaming, isDreamingWithOverlay) - .sample( + .combine( dozeTransitionModel, { isDreaming, dozeTransitionModel -> isDreaming && isDozeOff(dozeTransitionModel.to) } ) + .flatMapLatest { isAbleToDream -> + flow { + delay(50) + emit(isAbleToDream) + } + } .distinctUntilChanged() /** Whether the keyguard is showing or not. */ @@ -103,4 +141,21 @@ constructor( fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() } + + private fun cameraLaunchSourceIntToModel(value: Int): CameraLaunchSourceModel { + return when (value) { + StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE -> CameraLaunchSourceModel.WIGGLE + StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP -> + CameraLaunchSourceModel.POWER_DOUBLE_TAP + StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER -> + CameraLaunchSourceModel.LIFT_TRIGGER + StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE -> + CameraLaunchSourceModel.QUICK_AFFORDANCE + else -> throw IllegalArgumentException("Invalid CameraLaunchSourceModel value: $value") + } + } + + companion object { + private const val TAG = "KeyguardInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index a2661d76d90d..d4e2349907bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -19,11 +19,14 @@ package com.android.systemui.keyguard.domain.interactor import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.log.LogLevel.VERBOSE import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +private val TAG = KeyguardTransitionAuditLogger::class.simpleName!! + /** Collect flows of interest for auditing keyguard transitions. */ @SysUISingleton class KeyguardTransitionAuditLogger @@ -37,35 +40,47 @@ constructor( fun start() { scope.launch { - keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) } + keyguardInteractor.wakefulnessModel.collect { + logger.log(TAG, VERBOSE, "WakefulnessModel", it) + } } scope.launch { - keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) } + keyguardInteractor.isBouncerShowing.collect { + logger.log(TAG, VERBOSE, "Bouncer showing", it) + } } - scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } } + scope.launch { + keyguardInteractor.isDozing.collect { logger.log(TAG, VERBOSE, "isDozing", it) } + } - scope.launch { keyguardInteractor.isDreaming.collect { logger.v("isDreaming", it) } } + scope.launch { + keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + } scope.launch { interactor.finishedKeyguardTransitionStep.collect { - logger.i("Finished transition", it) + logger.log(TAG, VERBOSE, "Finished transition", it) } } scope.launch { interactor.canceledKeyguardTransitionStep.collect { - logger.i("Canceled transition", it) + logger.log(TAG, VERBOSE, "Canceled transition", it) } } scope.launch { - interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) } + interactor.startedKeyguardTransitionStep.collect { + logger.log(TAG, VERBOSE, "Started transition", it) + } } scope.launch { - keyguardInteractor.dozeTransitionModel.collect { logger.i("Doze transition", it) } + keyguardInteractor.dozeTransitionModel.collect { + logger.log(TAG, VERBOSE, "Doze transition", it) + } } } } 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 index 04024be571c8..ad6dbea7ae43 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -22,13 +22,17 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState 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.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min import kotlin.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter @@ -42,24 +46,39 @@ class KeyguardTransitionInteractor constructor( repository: KeyguardTransitionRepository, ) { + /** (any)->AOD transition information */ + val anyStateToAodTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == KeyguardState.AOD } + /** AOD->LOCKSCREEN transition information. */ val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) - /** LOCKSCREEN->AOD transition information. */ - val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) - /** DREAMING->LOCKSCREEN transition information. */ val dreamingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DREAMING, LOCKSCREEN) + /** GONE->DREAMING transition information. */ + val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING) + + /** LOCKSCREEN->AOD transition information. */ + val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + + /** LOCKSCREEN->BOUNCER transition information. */ + val lockscreenToBouncerTransition: Flow<TransitionStep> = + repository.transition(LOCKSCREEN, BOUNCER) + + /** LOCKSCREEN->DREAMING transition information. */ + val lockscreenToDreamingTransition: Flow<TransitionStep> = + repository.transition(LOCKSCREEN, DREAMING) + + /** LOCKSCREEN->OCCLUDED transition information. */ + val lockscreenToOccludedTransition: Flow<TransitionStep> = + repository.transition(LOCKSCREEN, OCCLUDED) + /** OCCLUDED->LOCKSCREEN transition information. */ val occludedToLockscreenTransition: Flow<TransitionStep> = repository.transition(OCCLUDED, LOCKSCREEN) - /** (any)->AOD transition information */ - val anyStateToAodTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == KeyguardState.AOD } - /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> * Lockscreen (0f). @@ -98,13 +117,23 @@ constructor( ): Flow<Float> { val start = (params.startTime / totalDuration).toFloat() val chunks = (totalDuration / params.duration).toFloat() + var isRunning = false return flow - // When starting, emit a value of 0f to give animations a chance to set initial state .map { step -> + val value = (step.value - start) * chunks if (step.transitionState == STARTED) { - 0f + // When starting, make sure to always emit. If a transition is started from the + // middle, it is possible this animation is being skipped but we need to inform + // the ViewModels of the last update + isRunning = true + max(0f, min(1f, value)) + } else if (isRunning && value >= 1f) { + // Always send a final value of 1. Because of rounding, [value] may never be + // exactly 1. + isRunning = false + 1f } else { - (step.value - start) * chunks + value } } .filter { value -> value >= 0f && value <= 1f } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt index c5e49c61e581..3099a497bf45 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt @@ -18,27 +18,29 @@ package com.android.systemui.keyguard.domain.interactor import android.view.View import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.phone.KeyguardBouncer +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.util.ListenerSet import javax.inject.Inject /** Interactor to add and remove callbacks for the bouncer. */ @SysUISingleton class PrimaryBouncerCallbackInteractor @Inject constructor() { - private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>() - private var expansionCallbacks = ArrayList<KeyguardBouncer.PrimaryBouncerExpansionCallback>() + private var resetCallbacks = ListenerSet<KeyguardResetCallback>() + private var expansionCallbacks = ArrayList<PrimaryBouncerExpansionCallback>() + /** Add a KeyguardResetCallback. */ - fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) { + fun addKeyguardResetCallback(callback: KeyguardResetCallback) { resetCallbacks.addIfAbsent(callback) } /** Remove a KeyguardResetCallback. */ - fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) { + fun removeKeyguardResetCallback(callback: KeyguardResetCallback) { resetCallbacks.remove(callback) } /** Adds a callback to listen to bouncer expansion updates. */ - fun addBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) { + fun addBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) { if (!expansionCallbacks.contains(callback)) { expansionCallbacks.add(callback) } @@ -48,7 +50,7 @@ class PrimaryBouncerCallbackInteractor @Inject constructor() { * Removes a previously added callback. If the callback was never added, this method does * nothing. */ - fun removeBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) { + fun removeBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) { expansionCallbacks.remove(callback) } @@ -99,4 +101,40 @@ class PrimaryBouncerCallbackInteractor @Inject constructor() { callback.onKeyguardReset() } } + + /** Callback updated when the primary bouncer's show and hide states change. */ + interface PrimaryBouncerExpansionCallback { + /** + * Invoked when the bouncer expansion reaches [EXPANSION_VISIBLE]. This is NOT called each + * time the bouncer is shown, but rather only when the fully shown amount has changed based + * on the panel expansion. The bouncer's visibility can still change when the expansion + * amount hasn't changed. See [PrimaryBouncerInteractor.isFullyShowing] for the checks for + * the bouncer showing state. + */ + fun onFullyShown() {} + + /** Invoked when the bouncer is starting to transition to a hidden state. */ + fun onStartingToHide() {} + + /** Invoked when the bouncer is starting to transition to a visible state. */ + fun onStartingToShow() {} + + /** Invoked when the bouncer expansion reaches [EXPANSION_HIDDEN]. */ + fun onFullyHidden() {} + + /** + * From 0f [EXPANSION_VISIBLE] when fully visible to 1f [EXPANSION_HIDDEN] when fully hidden + */ + fun onExpansionChanged(bouncerHideAmount: Float) {} + + /** + * Invoked when visibility of KeyguardBouncer has changed. Note the bouncer expansion can be + * [EXPANSION_VISIBLE], but the view's visibility can be [View.INVISIBLE]. + */ + fun onVisibilityChanged(isVisible: Boolean) {} + } + + interface KeyguardResetCallback { + fun onKeyguardReset() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index 2cf5fb98d07e..a92540d733b5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -32,11 +32,11 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.shared.system.SysUiStatsLog -import com.android.systemui.statusbar.phone.KeyguardBouncer import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -143,7 +143,7 @@ constructor( Trace.beginSection("KeyguardBouncer#show") repository.setPrimaryScrimmed(isScrimmed) if (isScrimmed) { - setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE) + setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) } if (resumeBouncer) { @@ -204,14 +204,14 @@ constructor( } if ( - expansion == KeyguardBouncer.EXPANSION_VISIBLE && - oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE + expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE && + oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE ) { falsingCollector.onBouncerShown() primaryBouncerCallbackInteractor.dispatchFullyShown() } else if ( - expansion == KeyguardBouncer.EXPANSION_HIDDEN && - oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN + expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN && + oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN ) { /* * There are cases where #hide() was not invoked, such as when @@ -222,8 +222,8 @@ constructor( DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() } primaryBouncerCallbackInteractor.dispatchFullyHidden() } else if ( - expansion != KeyguardBouncer.EXPANSION_VISIBLE && - oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE + expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE && + oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE ) { primaryBouncerCallbackInteractor.dispatchStartingToHide() repository.setPrimaryStartingToHide(true) @@ -303,7 +303,7 @@ constructor( fun isFullyShowing(): Boolean { return (repository.primaryBouncerShowingSoon.value || repository.primaryBouncerVisible.value) && - repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE && + repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE && repository.primaryBouncerStartingDisappearAnimation.value == null } @@ -315,8 +315,8 @@ constructor( /** If bouncer expansion is between 0f and 1f non-inclusive. */ fun isInTransit(): Boolean { return repository.primaryBouncerShowingSoon.value || - repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN && - repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE + repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN && + repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE } /** Return whether bouncer is animating away. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt new file mode 100644 index 000000000000..bb5ac84c6e54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.shared.constants + +object KeyguardBouncerConstants { + /** + * Values for the bouncer expansion represented as the panel expansion. Panel expansion 1f = + * panel fully showing = bouncer fully hidden Panel expansion 0f = panel fully hiding = bouncer + * fully showing + */ + const val EXPANSION_HIDDEN = 1f + const val EXPANSION_VISIBLE = 0f +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt new file mode 100644 index 000000000000..19baf7705546 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** Camera launch sources */ +enum class CameraLaunchSourceModel { + /** Device is wiggled */ + WIGGLE, + /** Power button has been double tapped */ + POWER_DOUBLE_TAP, + /** Device has been lifted */ + LIFT_TRIGGER, + /** Quick affordance button has been pressed */ + QUICK_AFFORDANCE, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt index 7d133598e105..e7e915940290 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.shared.model +import android.content.Intent import androidx.annotation.DrawableRes /** @@ -45,4 +46,7 @@ data class KeyguardQuickAffordancePickerRepresentation( * user to a destination where they can re-enable it. */ val actionComponentName: String? = null, + + /** Optional [Intent] to use to start an activity to configure this affordance. */ + val configureIntent: Intent? = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt new file mode 100644 index 000000000000..4fd14b1ce087 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.shared.model + +/** Represents the trust state */ +data class TrustModel( + /** If true, the system believes the environment to be trusted. */ + val isTrusted: Boolean, + /** The user, for which the trust changed. */ + val userId: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index 0e4058bf8f6d..d020529d2bae 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -45,7 +45,6 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewMod import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.kotlin.pairwise import kotlin.math.pow import kotlin.math.sqrt import kotlin.time.Duration.Companion.milliseconds @@ -129,18 +128,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.startButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.endButton.collect { buttonModel -> updateButton( view = endButton, @@ -153,18 +140,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.endButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.isOverlayContainerVisible.collect { isVisible -> overlayContainer.visibility = if (isVisible) { @@ -367,14 +342,12 @@ object KeyguardBottomAreaViewBinder { private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() private var longPressAnimator: ViewPropertyAnimator? = null - private var downTimestamp = 0L @SuppressLint("ClickableViewAccessibility") override fun onTouch(v: View?, event: MotionEvent?): Boolean { return when (event?.actionMasked) { MotionEvent.ACTION_DOWN -> if (viewModel.configKey != null) { - downTimestamp = System.currentTimeMillis() longPressAnimator = view .animate() @@ -383,6 +356,13 @@ object KeyguardBottomAreaViewBinder { .setDuration(longPressDurationMs) .withEndAction { view.setOnClickListener { + vibratorHelper?.vibrate( + if (viewModel.isActivated) { + Vibrations.Activated + } else { + Vibrations.Deactivated + } + ) viewModel.onClicked( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = viewModel.configKey, @@ -414,7 +394,7 @@ object KeyguardBottomAreaViewBinder { MotionEvent.ACTION_UP -> { cancel( onAnimationEnd = - if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + if (event.eventTime - event.downTime < longPressDurationMs) { Runnable { messageDisplayer.invoke( R.string.keyguard_affordance_press_too_short @@ -455,7 +435,6 @@ object KeyguardBottomAreaViewBinder { } private fun cancel(onAnimationEnd: Runnable? = null) { - downTimestamp = 0L longPressAnimator?.cancel() longPressAnimator = null view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index f772b17a7fb6..5e46c5d1f67a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.view.KeyEvent import android.view.View import android.view.ViewGroup +import android.window.OnBackAnimationCallback import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.internal.policy.SystemBarUtils @@ -27,10 +28,10 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.dagger.KeyguardBouncerComponent import com.android.systemui.keyguard.data.BouncerViewDelegate +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -55,6 +56,10 @@ object KeyguardBouncerViewBinder { mode == KeyguardSecurityModel.SecurityMode.SimPuk } + override fun getBackCallback(): OnBackAnimationCallback { + return hostViewController.backCallback + } + override fun shouldDismissOnMenuPressed(): Boolean { return hostViewController.shouldEnableMenuKey() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt index e164f5d58b07..6627865ecc79 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -22,10 +22,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to @@ -49,9 +53,15 @@ constructor( /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow<Float> { - return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) - } + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.dreamingToLockscreenTransition + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } + .map { 0f } + ) } /** Lockscreen views alpha */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt new file mode 100644 index 000000000000..5a4796096eeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */ +@SysUISingleton +class GoneToDreamingTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.goneToDreamingTransition + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } + .map { 0f } + ) + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.goneToDreamingTransition, + params, + totalDuration = TO_DREAMING_DURATION + ) + } + + companion object { + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) + val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt index e5d4e4971baa..c6002d6db91a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt @@ -20,9 +20,9 @@ import android.view.View import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel -import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt new file mode 100644 index 000000000000..e05adbdab583 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** + * Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class LockscreenToDreamingTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.lockscreenToDreamingTransition + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } + .map { 0f } + ) + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.lockscreenToDreamingTransition, + params, + totalDuration = TO_DREAMING_DURATION + ) + } + + companion object { + @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds + + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) + val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt new file mode 100644 index 000000000000..22d292e92856 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** + * Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class LockscreenToOccludedTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.lockscreenToOccludedTransition + .filter { step -> step.transitionState == TransitionState.FINISHED } + .map { 0f } + ) + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.lockscreenToOccludedTransition, + params, + totalDuration = TO_OCCLUDED_DURATION + ) + } + + companion object { + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_OCCLUDED_DURATION) + val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index e3649187b0a7..d69ac7fe035d 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -145,7 +145,7 @@ private fun createLifecycleOwnerAndRun( * └───────────────┴───────────────────┴──────────────┴─────────────────┘ * ``` */ -private class ViewLifecycleOwner( +class ViewLifecycleOwner( private val view: View, ) : LifecycleOwner { diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt index 0645236226bd..9f563fe4eae5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt @@ -23,3 +23,15 @@ import javax.inject.Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for small keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardSmallClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for large keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardLargeClockLog 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 bc29858c5b92..d7817e1dc4f0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -335,13 +335,33 @@ public class LogModule { } /** - * Provides a {@link LogBuffer} for keyguard clock logs. + * Provides a {@link LogBuffer} for general keyguard clock logs. */ @Provides @SysUISingleton @KeyguardClockLog public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { - return factory.create("KeyguardClockLog", 500); + return factory.create("KeyguardClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard small clock logs. + */ + @Provides + @SysUISingleton + @KeyguardSmallClockLog + public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) { + return factory.create("KeyguardSmallClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard large clock logs. + */ + @Provides + @SysUISingleton + @KeyguardLargeClockLog + public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) { + return factory.create("KeyguardLargeClockLog", 100); } /** diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 7a90a7470cd2..7ccc43ce62c2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -29,6 +29,18 @@ constructor( private val dumpManager: DumpManager, private val systemClock: SystemClock, ) { + private val existingBuffers = mutableMapOf<String, TableLogBuffer>() + + /** + * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where + * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of + * obtaining a buffer. + * + * @param name a unique table name + * @param maxSize the buffer max size. See [adjustMaxSize] + * + * @return a new [TableLogBuffer] registered with [DumpManager] + */ fun create( name: String, maxSize: Int, @@ -37,4 +49,23 @@ constructor( dumpManager.registerNormalDumpable(name, tableBuffer) return tableBuffer } + + /** + * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in + * bugreports. Because of this, many of them are created statically in the Dagger graph. + * + * In the case where you have to create a logbuffer with a name only known at runtime, this + * method can be used to lazily create a table log buffer which is then cached for reuse. + * + * @return a [TableLogBuffer] suitable for reuse + */ + fun getOrCreate( + name: String, + maxSize: Int, + ): TableLogBuffer = + existingBuffers.getOrElse(name) { + val buffer = create(name, maxSize) + existingBuffers[name] = buffer + buffer + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index ceb48458a1d3..a692ad74615f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -18,6 +18,7 @@ package com.android.systemui.media import android.app.ActivityOptions import android.content.Intent import android.content.res.Configuration +import android.content.res.Resources import android.media.projection.IMediaProjection import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION import android.os.Binder @@ -27,6 +28,7 @@ import android.os.ResultReceiver import android.os.UserHandle import android.view.ViewGroup import com.android.internal.annotations.VisibleForTesting +import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider import com.android.internal.app.ChooserActivity import com.android.internal.app.ResolverListController import com.android.internal.app.chooser.NotSelectableTargetInfo @@ -59,16 +61,12 @@ class MediaProjectionAppSelectorActivity( private lateinit var configurationController: ConfigurationController private lateinit var controller: MediaProjectionAppSelectorController private lateinit var recentsViewController: MediaProjectionRecentsViewController + private lateinit var component: MediaProjectionAppSelectorComponent override fun getLayoutResource() = R.layout.media_projection_app_selector public override fun onCreate(bundle: Bundle?) { - val component = - componentFactory.create( - activity = this, - view = this, - resultHandler = this - ) + component = componentFactory.create(activity = this, view = this, resultHandler = this) // Create a separate configuration controller for this activity as the configuration // might be different from the global one @@ -76,11 +74,12 @@ class MediaProjectionAppSelectorActivity( controller = component.controller recentsViewController = component.recentsViewController - val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) } - intent.putExtra(Intent.EXTRA_INTENT, queryIntent) + intent.configureChooserIntent( + resources, + component.hostUserHandle, + component.personalProfileUserHandle + ) - val title = getString(R.string.media_projection_permission_app_selector_title) - intent.putExtra(Intent.EXTRA_TITLE, title) super.onCreate(bundle) controller.init() } @@ -183,6 +182,13 @@ class MediaProjectionAppSelectorActivity( override fun shouldShowContentPreview() = true + override fun shouldShowContentPreviewWhenEmpty(): Boolean = true + + override fun createMyUserIdProvider(): MyUserIdProvider = + object : MyUserIdProvider() { + override fun getMyUserId(): Int = component.hostUserHandle.identifier + } + override fun createContentPreviewView(parent: ViewGroup): ViewGroup = recentsViewController.createView(parent) @@ -193,6 +199,34 @@ class MediaProjectionAppSelectorActivity( * instance through activity result. */ const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver" + + /** UID of the app that originally launched the media projection flow (host app user) */ + const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle" const val KEY_CAPTURE_TARGET = "capture_region" + + /** Set up intent for the [ChooserActivity] */ + private fun Intent.configureChooserIntent( + resources: Resources, + hostUserHandle: UserHandle, + personalProfileUserHandle: UserHandle + ) { + // Specify the query intent to show icons for all apps on the chooser screen + val queryIntent = + Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) } + putExtra(Intent.EXTRA_INTENT, queryIntent) + + // Update the title of the chooser + val title = resources.getString(R.string.media_projection_permission_app_selector_title) + putExtra(Intent.EXTRA_TITLE, title) + + // Select host app's profile tab by default + val selectedProfile = + if (hostUserHandle == personalProfileUserHandle) { + PROFILE_PERSONAL + } else { + PROFILE_WORK + } + putExtra(EXTRA_SELECTED_PROFILE, selectedProfile) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java index bfa67a89baca..d830fc4a5fb5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -22,6 +22,7 @@ import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREE import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP; import android.app.Activity; +import android.app.ActivityManager; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; @@ -35,6 +36,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.text.BidiFormatter; import android.text.SpannableString; import android.text.TextPaint; @@ -208,8 +210,14 @@ public class MediaProjectionPermissionActivity extends Activity final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class); intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder()); + intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, + UserHandle.getUserHandleForUid(getLaunchedFromUid())); intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - startActivity(intent); + + // Start activity from the current foreground user to avoid creating a separate + // SystemUI process without access to recent tasks because it won't have + // WM Shell running inside. + startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser())); } } catch (RemoteException e) { Log.e(TAG, "Error granting projection permission", e); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index f006442906e7..be18cbec7163 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -88,7 +88,10 @@ data class MediaData( val instanceId: InstanceId, /** The UID of the app, used for logging */ - val appUid: Int + val appUid: Int, + + /** Whether explicit indicator exists */ + val isExplicit: Boolean = false, ) { companion object { /** Media is playing on the local device */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a8f39fa9a456..1c8bfd1fc468 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier +import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -44,6 +45,7 @@ class MediaViewHolder constructor(itemView: View) { val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) + val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator) // Output switcher val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) @@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) { R.id.app_name, R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.media_seamless, R.id.media_progress_bar, R.id.actionPlayPause, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 2dd339d409a6..a13279717d05 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -45,6 +45,7 @@ import android.os.Process import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants @@ -660,6 +661,10 @@ class MediaDataManager( val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = currentEntry?.appUid ?: Process.INVALID_UID + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && + mediaFlags.isExplicitIndicatorEnabled() val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() @@ -689,7 +694,8 @@ class MediaDataManager( hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } @@ -750,6 +756,15 @@ class MediaDataManager( song = HybridGroupManager.resolveTitle(notif) } + // Explicit Indicator + var isExplicit = false + if (mediaFlags.isExplicitIndicatorEnabled()) { + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + } + // Artist name var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { @@ -848,10 +863,11 @@ class MediaDataManager( notificationKey = key, hasCheckedForResume = hasCheckedForResume, isPlaying = isPlaying, - isClearable = sbn.isClearable(), + isClearable = !sbn.isOngoing, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt index 93be6a78ccd5..5c65c8bd5689 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -162,8 +162,8 @@ internal constructor( context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES ) - colorScheme.accent1[2] - else colorScheme.accent1[3] + colorScheme.accent1.s100 + else colorScheme.accent1.s200 }, { seamlessColor: Int -> val accentColorList = ColorStateList.valueOf(seamlessColor) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt index 899148b0014c..8f1c9048026f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt @@ -130,7 +130,12 @@ constructor( private var splitShadeContainer: ViewGroup? = null /** Track the media player setting status on lock screen. */ - private var allowMediaPlayerOnLockScreen: Boolean = true + private var allowMediaPlayerOnLockScreen: Boolean = + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT + ) private val lockScreenMediaPlayerUri = secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index 1fdbc99333cb..e7f7647797cd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -94,7 +94,7 @@ constructor( private var currentCarouselWidth: Int = 0 /** The current height of the carousel */ - private var currentCarouselHeight: Int = 0 + @VisibleForTesting var currentCarouselHeight: Int = 0 /** Are we currently showing only active players */ private var currentlyShowingOnlyActive: Boolean = false @@ -128,14 +128,14 @@ constructor( /** The measured height of the carousel */ private var carouselMeasureHeight: Int = 0 private var desiredHostState: MediaHostState? = null - private val mediaCarousel: MediaScrollView + @VisibleForTesting var mediaCarousel: MediaScrollView val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup @VisibleForTesting lateinit var settingsButton: View private set private val mediaContent: ViewGroup - @VisibleForTesting val pageIndicator: PageIndicator + @VisibleForTesting var pageIndicator: PageIndicator private val visualStabilityCallback: OnReorderingAllowedListener private var needsReordering: Boolean = false private var keysNeedRemoval = mutableSetOf<String>() @@ -160,30 +160,26 @@ constructor( } companion object { - const val ANIMATION_BASE_DURATION = 2200f - const val DURATION = 167f - const val DETAILS_DELAY = 1067f - const val CONTROLS_DELAY = 1400f - const val PAGINATION_DELAY = 1900f - const val MEDIATITLES_DELAY = 1000f - const val MEDIACONTAINERS_DELAY = 967f val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) - val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F) - - fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { - val transformStartFraction = delay / ANIMATION_BASE_DURATION - val transformDurationFraction = duration / ANIMATION_BASE_DURATION - val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) - return MathUtils.constrain( - (squishinessToTime - transformStartFraction) / transformDurationFraction, - 0F, - 1F - ) + + fun calculateAlpha( + squishinessFraction: Float, + startPosition: Float, + endPosition: Float + ): Float { + val transformFraction = + MathUtils.constrain( + (squishinessFraction - startPosition) / (endPosition - startPosition), + 0F, + 1F + ) + return TRANSFORM_BEZIER.getInterpolation(transformFraction) } } private val configListener = object : ConfigurationController.ConfigurationListener { + var lastOrientation = -1 override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may @@ -200,7 +196,13 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { if (newConfig == null) return isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL - updatePlayers(recreateMedia = true) + val newOrientation = newConfig.orientation + if (lastOrientation != newOrientation) { + // The players actually depend on the orientation possibly, so we have to + // recreate them (at least on large screen devices) + lastOrientation = newOrientation + updatePlayers(recreateMedia = true) + } } override fun onUiModeChanged() { @@ -717,6 +719,9 @@ constructor( private fun updatePlayers(recreateMedia: Boolean) { pageIndicator.tintList = ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) + val previousVisibleKey = + MediaPlayerData.visiblePlayerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> if (isSsMediaRec) { @@ -741,6 +746,9 @@ constructor( isSsReactivated = isSsReactivated ) } + if (recreateMedia) { + reorderAllPlayers(previousVisibleKey) + } } } @@ -800,7 +808,12 @@ constructor( val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F val endAlpha = (if (endIsVisible) 1.0f else 0.0f) * - calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) + calculateAlpha( + squishFraction, + (pageIndicator.translationY + pageIndicator.height) / + mediaCarousel.measuredHeight, + 1F + ) var alpha = 1.0f if (!endIsVisible || !startIsVisible) { var progress = currentTransitionProgress @@ -826,7 +839,8 @@ constructor( pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams pageIndicator.translationY = - (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() + (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin) + .toFloat() } /** Update the dimension of this carousel. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt index 82abf9bfcc80..2a8362b64cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt @@ -19,28 +19,28 @@ package com.android.systemui.media.controls.ui import com.android.systemui.monet.ColorScheme /** Returns the surface color for media controls based on the scheme. */ -internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2[9] // A2-800 +internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2.s800 // A2-800 /** Returns the primary accent color for media controls based on the scheme. */ -internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1[2] // A1-100 +internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1.s100 // A1-100 /** Returns the secondary accent color for media controls based on the scheme. */ -internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1[3] // A1-200 +internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1.s200 // A1-200 /** Returns the primary text color for media controls based on the scheme. */ -internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1[1] // N1-50 +internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1.s50 // N1-50 /** Returns the inverse of the primary text color for media controls based on the scheme. */ -internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1[10] // N1-900 +internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1.s900 // N1-900 /** Returns the secondary text color for media controls based on the scheme. */ -internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2[3] // N2-200 +internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s200 // N2-200 /** Returns the tertiary text color for media controls based on the scheme. */ -internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2[5] // N2-400 +internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s400 // N2-400 /** Returns the color for the start of the background gradient based on the scheme. */ -internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2[8] // A2-700 +internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2.s700 // A2-700 /** Returns the color for the end of the background gradient based on the scheme. */ -internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1[8] // A1-700 +internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1.s700 // A1-700 diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 15c34430f455..45d50f0e4976 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -50,7 +50,6 @@ import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -68,6 +67,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; +import com.android.internal.widget.CachingIconView; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.R; @@ -111,8 +111,11 @@ import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimat import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; +import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; +import dagger.Lazy; + import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -120,7 +123,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; +import kotlin.Triple; import kotlin.Unit; /** @@ -166,10 +169,13 @@ public class MediaControlPanel { R.id.action1 ); + // Time in millis for playing turbulence noise that is played after a touch ripple. + @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L; + private final SeekBarViewModel mSeekBarViewModel; private SeekBarObserver mSeekBarObserver; protected final Executor mBackgroundExecutor; - private final Executor mMainExecutor; + private final DelayableExecutor mMainExecutor; private final ActivityStarter mActivityStarter; private final BroadcastSender mBroadcastSender; @@ -222,10 +228,10 @@ public class MediaControlPanel { private String mSwitchBroadcastApp; private MultiRippleController mMultiRippleController; private TurbulenceNoiseController mTurbulenceNoiseController; - private FeatureFlags mFeatureFlags; - private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null; + private final FeatureFlags mFeatureFlags; + private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig; @VisibleForTesting - MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener = null; + MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener; /** * Initialize a new control panel @@ -239,7 +245,7 @@ public class MediaControlPanel { public MediaControlPanel( Context context, @Background Executor backgroundExecutor, - @Main Executor mainExecutor, + @Main DelayableExecutor mainExecutor, ActivityStarter activityStarter, BroadcastSender broadcastSender, MediaViewController mediaViewController, @@ -398,10 +404,11 @@ public class MediaControlPanel { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator(); AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter, - Interpolators.EMPHASIZED_DECELERATE, titleText, artistText); + Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator); AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, - Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); + Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator); MultiRippleView multiRippleView = vh.getMultiRippleView(); mMultiRippleController = new MultiRippleController(multiRippleView); @@ -409,10 +416,12 @@ public class MediaControlPanel { if (mFeatureFlags.isEnabled(Flags.UMO_TURBULENCE_NOISE)) { mRipplesFinishedListener = () -> { if (mTurbulenceNoiseAnimationConfig == null) { - mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation(); + mTurbulenceNoiseAnimationConfig = createTurbulenceNoiseAnimation(); } // Color will be correctly updated in ColorSchemeTransition. mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig); + mMainExecutor.executeDelayed( + mTurbulenceNoiseController::finish, TURBULENCE_NOISE_PLAY_DURATION); }; mMultiRippleController.addRipplesFinishedListener(mRipplesFinishedListener); } @@ -664,11 +673,15 @@ public class MediaControlPanel { private boolean bindSongMetadata(MediaData data) { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); + ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); return mMetadataAnimationHandler.setNext( - Pair.create(data.getSong(), data.getArtist()), + new Triple(data.getSong(), data.getArtist(), data.isExplicit()), () -> { titleText.setText(data.getSong()); artistText.setText(data.getArtist()); + setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit()); + setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit()); // refreshState is required here to resize the text views (and prevent ellipsis) mMediaViewController.refreshState(); @@ -1056,7 +1069,7 @@ public class MediaControlPanel { ); } - private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() { + private TurbulenceNoiseAnimationConfig createTurbulenceNoiseAnimation() { return new TurbulenceNoiseAnimationConfig( TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT, TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, @@ -1071,7 +1084,9 @@ public class MediaControlPanel { /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(), /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(), TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS, + /* easeInDuration= */ TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS, + /* easeOutDuration= */ TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS, this.getContext().getResources().getDisplayMetrics().density, BlendMode.PLUS, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index f7a9bc760caf..66f12d6242b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -41,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeStateEvents @@ -93,6 +94,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, private val mediaCarouselController: MediaCarouselController, + private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, configurationController: ConfigurationController, @@ -224,9 +226,9 @@ constructor( private var inSplitShade = false - /** Is there any active media in the carousel? */ - private var hasActiveMedia: Boolean = false - get() = mediaHosts.get(LOCATION_QQS)?.visible == true + /** Is there any active media or recommendation in the carousel? */ + private var hasActiveMediaOrRecommendation: Boolean = false + get() = mediaManager.hasActiveMediaOrRecommendation() /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false @@ -582,12 +584,8 @@ constructor( val viewHost = createUniqueObjectHost() mediaObject.hostView = viewHost mediaObject.addVisibilityChangeListener { - // If QQS changes visibility, we need to force an update to ensure the transition - // goes into the correct state - val stateUpdate = mediaObject.location == LOCATION_QQS - // Never animate because of a visibility change, only state changes should do that - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate) + updateDesiredLocation(forceNoAnimation = true) } mediaHosts[mediaObject.location] = mediaObject if (mediaObject.location == desiredLocation) { @@ -908,7 +906,7 @@ constructor( fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation) } private fun hasValidStartAndEndLocations(): Boolean { @@ -965,7 +963,7 @@ constructor( private fun getQSTransformationProgress(): Float { val currentHost = getHost(desiredLocation) val previousHost = getHost(previousLocation) - if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) { + if (currentHost?.location == LOCATION_QS && !inSplitShade) { if (previousHost?.location == LOCATION_QQS) { if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) { return qsExpansion @@ -1028,7 +1026,8 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() + // Don't use the overlay when fading or when we don't have active media + var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation if (isCrossFadeAnimatorRunning) { if ( getHost(newLocation)?.visible == true && @@ -1122,7 +1121,6 @@ constructor( dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 322421318cb8..2ec7be6eaa32 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -24,11 +24,6 @@ import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.MeasurementOutput @@ -36,6 +31,8 @@ import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayoutController import com.android.systemui.util.animation.TransitionViewState import com.android.systemui.util.traceSection +import java.lang.Float.max +import java.lang.Float.min import javax.inject.Inject /** @@ -80,6 +77,7 @@ constructor( setOf( R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.actionPlayPause, ) @@ -304,39 +302,106 @@ constructor( val squishedViewState = viewState.copy() val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt() squishedViewState.height = squishedHeight - controlIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION) - } - } - - detailIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION) - } - } - // We are not overriding the squishedViewStates height but only the children to avoid // them remeasuring the whole view. Instead it just remains as the original size backgroundIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.height = squishedHeight - } + squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } } - RecommendationViewHolder.mediaContainersIds.forEach { id -> + // media player + val controlsTop = + calculateWidgetGroupAlphaForSquishiness( + controlIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + detailIds, + controlsTop, + squishedViewState, + squishFraction + ) + // recommendation card + val titlesTop = + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaTitlesAndSubtitlesIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaContainersIds, + titlesTop, + squishedViewState, + squishFraction + ) + return squishedViewState + } + + /** + * This function is to make each widget in UMO disappear before being clipped by squished UMO + * + * The general rule is that widgets in UMO has been divided into several groups, and widgets in + * one group have the same alpha during squishing It will change from alpha 0.0 when the visible + * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible + * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause + * button will change alpha together. + * ``` + * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls, + * including progress bar, next button, previous button + * ``` + * widgetGroupIds: a group of widgets have same state during UMO is squished, + * ``` + * e.g. Album title, artist title and play-pause button + * ``` + * groupEndPosition: the height of UMO, when the height reaches this value, + * ``` + * widgets in this group should have 1.0 as alpha + * e.g., the group of album title, artist title and play-pause button will become fully + * visible when the height of UMO reaches the top of controls group + * (progress bar, previous button and next button) + * ``` + * squishedViewState: hold the widgetState of each widget, which will be modified + * squishFraction: the squishFraction of UMO + */ + private fun calculateWidgetGroupAlphaForSquishiness( + widgetGroupIds: Set<Int>, + groupEndPosition: Float, + squishedViewState: TransitionViewState, + squishFraction: Float + ): Float { + val nonsquishedHeight = squishedViewState.measureHeight + var groupTop = squishedViewState.measureHeight.toFloat() + var groupBottom = 0F + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION) + groupTop = min(groupTop, state.y) + groupBottom = max(groupBottom, state.y + state.height) } } - - RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id -> + // startPosition means to the height of squished UMO where the widget alpha should start + // changing from 0.0 + // generally, it equals to the bottom of widgets, so that we can meet the requirement that + // widget should not go beyond the bounds of background + // endPosition means to the height of squished UMO where the widget alpha should finish + // changing alpha to 1.0 + var startPosition = groupBottom + val endPosition = groupEndPosition + if (startPosition == endPosition) { + startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat() + } + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION) + state.alpha = + calculateAlpha( + squishFraction, + startPosition / nonsquishedHeight, + endPosition / nonsquishedHeight + ) } } - - return squishedViewState + return groupTop // used for the widget group above this group } /** @@ -544,11 +609,13 @@ constructor( overrideSize?.let { // To be safe we're using a maximum here. The override size should always be set // properly though. - if (result.measureHeight != it.measuredHeight - || result.measureWidth != it.measuredWidth) { + if ( + result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth + ) { result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) - // The measureHeight and the shown height should both be set to the overridden height + // The measureHeight and the shown height should both be set to the overridden + // height result.height = result.measureHeight result.width = result.measureWidth // Make sure all background views are also resized such that their size is correct diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 8d4931a5d08c..5bc35caed515 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -42,4 +42,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. */ fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) + + /** Check whether we show explicit indicator on UMO */ + fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 8eb25c4c2495..41fca554e37d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -493,20 +493,20 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors, isDarkTheme); if (isDarkTheme) { - mColorItemContent = mCurrentColorScheme.getAccent1().get(2); // A1-100 - mColorSeekbarProgress = mCurrentColorScheme.getAccent2().get(7); // A2-600 - mColorButtonBackground = mCurrentColorScheme.getAccent1().get(4); // A1-300 - mColorItemBackground = mCurrentColorScheme.getNeutral2().get(9); // N2-800 - mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().get(9); // A2-800 - mColorPositiveButtonText = mCurrentColorScheme.getAccent2().get(9); // A2-800 - mColorDialogBackground = mCurrentColorScheme.getNeutral1().get(10); // N1-900 + mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100 + mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600 + mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300 + mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800 + mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800 + mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800 + mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900 } else { - mColorItemContent = mCurrentColorScheme.getAccent1().get(9); // A1-800 - mColorSeekbarProgress = mCurrentColorScheme.getAccent1().get(4); // A1-300 - mColorButtonBackground = mCurrentColorScheme.getAccent1().get(7); // A1-600 - mColorItemBackground = mCurrentColorScheme.getAccent2().get(1); // A2-50 - mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().get(2); // A1-100 - mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().get(1); // N1-50 + mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800 + mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300 + mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600 + mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50 + mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100 + mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50 mColorDialogBackground = mCurrentColorScheme.getBackgroundColor(); } } @@ -630,50 +630,28 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - //TODO(b/257851968): do the organization only when there's no suggested sorted order - // we get from application - attachRangeInfo(devices); - Collections.sort(devices, Comparator.naturalOrder()); + if (!isRouteProcessSupported() || (isRouteProcessSupported() + && !mLocalMediaManager.isPreferenceRouteListingExist())) { + attachRangeInfo(devices); + Collections.sort(devices, Comparator.naturalOrder()); + } // For the first time building list, to make sure the top device is the connected // device. + boolean needToHandleMutingExpectedDevice = + hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); + final MediaDevice connectedMediaDevice = + needToHandleMutingExpectedDevice ? null + : getCurrentConnectedMediaDevice(); if (mMediaItemList.isEmpty()) { - boolean needToHandleMutingExpectedDevice = - hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); - final MediaDevice connectedMediaDevice = - needToHandleMutingExpectedDevice ? null - : getCurrentConnectedMediaDevice(); if (connectedMediaDevice == null) { if (DEBUG) { Log.d(TAG, "No connected media device or muting expected device exist."); } - if (needToHandleMutingExpectedDevice) { - for (MediaDevice device : devices) { - if (device.isMutingExpectedDevice()) { - mMediaItemList.add(0, new MediaItem(device)); - mMediaItemList.add(1, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - mMediaItemList.add(new MediaItem()); - } else { - mMediaItemList.addAll( - devices.stream().map(MediaItem::new).collect(Collectors.toList())); - categorizeMediaItems(null); - } + categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice); return; } // selected device exist - for (MediaDevice device : devices) { - if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { - mMediaItemList.add(0, new MediaItem(device)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - categorizeMediaItems(connectedMediaDevice); + categorizeMediaItems(connectedMediaDevice, devices, false); return; } // To keep the same list order @@ -707,31 +685,46 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } } - private void categorizeMediaItems(MediaDevice connectedMediaDevice) { + private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices, + boolean needToHandleMutingExpectedDevice) { synchronized (mMediaDevicesLock) { Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map( MediaDevice::getId).collect(Collectors.toSet()); if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } - int latestSelected = 1; - for (MediaItem item : mMediaItemList) { - if (item.getMediaDevice().isPresent()) { - MediaDevice device = item.getMediaDevice().get(); - if (selectedDevicesIds.contains(device.getId())) { - latestSelected = mMediaItemList.indexOf(item) + 1; - } else { - mMediaItemList.add(latestSelected, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - break; + boolean suggestedDeviceAdded = false; + boolean displayGroupAdded = false; + for (MediaDevice device : devices) { + if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { + mMediaItemList.add(0, new MediaItem(device)); + } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains( + device.getId())) { + mMediaItemList.add(0, new MediaItem(device)); + } else { + if (device.isSuggestedDevice() && !suggestedDeviceAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_suggested_device)); + suggestedDeviceAdded = true; + } else if (!device.isSuggestedDevice() && !displayGroupAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_speakers_and_displays)); + displayGroupAdded = true; } + mMediaItemList.add(new MediaItem(device)); } } mMediaItemList.add(new MediaItem()); } } + private void attachGroupDivider(String title) { + synchronized (mMediaDevicesLock) { + mMediaItemList.add( + new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); + } + } + private void attachRangeInfo(List<MediaDevice> devices) { for (MediaDevice mediaDevice : devices) { if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) { @@ -751,6 +744,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT); } + public boolean isRouteProcessSupported() { + return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING); + } + List<MediaDevice> getGroupMediaDevices() { final List<MediaDevice> selectedDevices = getSelectedMediaDevice(); final List<MediaDevice> selectableDevices = getSelectableMediaDevice(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java new file mode 100644 index 000000000000..e35575bfc184 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.annotation.MainThread; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.statusbar.CommandQueue; + +import javax.inject.Inject; + +/** Controls display of media output switcher. */ +@SysUISingleton +public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue.Callbacks { + + private static final String TAG = "MediaOutputSwitcherDialogUI"; + + private final CommandQueue mCommandQueue; + private final MediaOutputDialogFactory mMediaOutputDialogFactory; + private final FeatureFlags mFeatureFlags; + + @Inject + public MediaOutputSwitcherDialogUI( + Context context, + CommandQueue commandQueue, + MediaOutputDialogFactory mediaOutputDialogFactory, + FeatureFlags featureFlags) { + mCommandQueue = commandQueue; + mMediaOutputDialogFactory = mediaOutputDialogFactory; + mFeatureFlags = featureFlags; + } + + @Override + public void start() { + if (mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_SHOW_API_ENABLED)) { + mCommandQueue.addCallback(this); + } else { + Log.w(TAG, "Show media output switcher is not enabled."); + } + } + + @Override + @MainThread + public void showMediaOutputSwitcher(String packageName) { + if (!TextUtils.isEmpty(packageName)) { + mMediaOutputDialogFactory.create(packageName, false, null); + } else { + Log.e(TAG, "Unable to launch media output dialog. Package name is empty."); + } + } +} 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 9f44d984124f..935f38de2e4f 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 @@ -150,7 +150,12 @@ constructor( logger: MediaTttLogger<ChipbarInfo>, ): ChipbarInfo { val packageName = routeInfo.clientPackageName - val otherDeviceName = routeInfo.name.toString() + val otherDeviceName = + if (routeInfo.name.isBlank()) { + context.getString(R.string.media_ttt_default_device_type) + } else { + routeInfo.name.toString() + } return ChipbarInfo( // Display the app's icon as the start icon diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt index 6c41caab38a8..1d863435fa6e 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -19,9 +19,11 @@ package com.android.systemui.mediaprojection.appselector import android.app.Activity import android.content.ComponentName import android.content.Context +import android.os.UserHandle import com.android.launcher3.icons.IconFactory import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.MediaProjectionAppSelectorActivity +import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader import com.android.systemui.mediaprojection.appselector.data.AppIconLoader import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader @@ -30,6 +32,8 @@ import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnail import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.policy.ConfigurationController import dagger.Binds @@ -39,6 +43,7 @@ import dagger.Provides import dagger.Subcomponent import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import java.lang.IllegalArgumentException import javax.inject.Qualifier import javax.inject.Scope import kotlinx.coroutines.CoroutineScope @@ -46,6 +51,12 @@ import kotlinx.coroutines.SupervisorJob @Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class HostUserHandle + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class PersonalProfile + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class WorkProfile + @Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope @Module(subcomponents = [MediaProjectionAppSelectorComponent::class]) @@ -83,7 +94,7 @@ interface MediaProjectionAppSelectorModule { @MediaProjectionAppSelector @MediaProjectionAppSelectorScope fun provideAppSelectorComponentName(context: Context): ComponentName = - ComponentName(context, MediaProjectionAppSelectorActivity::class.java) + ComponentName(context, MediaProjectionAppSelectorActivity::class.java) @Provides @MediaProjectionAppSelector @@ -93,9 +104,32 @@ interface MediaProjectionAppSelectorModule { ): ConfigurationController = ConfigurationControllerImpl(activity) @Provides - fun bindIconFactory( - context: Context - ): IconFactory = IconFactory.obtain(context) + @PersonalProfile + @MediaProjectionAppSelectorScope + fun personalUserHandle(activityManagerWrapper: ActivityManagerWrapper): UserHandle { + // Current foreground user is the 'personal' profile + return UserHandle.of(activityManagerWrapper.currentUserId) + } + + @Provides + @WorkProfile + @MediaProjectionAppSelectorScope + fun workProfileUserHandle(userTracker: UserTracker): UserHandle? = + userTracker.userProfiles.find { it.isManagedProfile }?.userHandle + + @Provides + @HostUserHandle + @MediaProjectionAppSelectorScope + fun hostUserHandle(activity: MediaProjectionAppSelectorActivity): UserHandle { + val extras = + activity.intent.extras + ?: error("MediaProjectionAppSelectorActivity should be launched with extras") + return extras.getParcelable(EXTRA_HOST_APP_USER_HANDLE) + ?: error("MediaProjectionAppSelectorActivity should be provided with " + + "$EXTRA_HOST_APP_USER_HANDLE extra") + } + + @Provides fun bindIconFactory(context: Context): IconFactory = IconFactory.obtain(context) @Provides @MediaProjectionAppSelector @@ -124,6 +158,8 @@ interface MediaProjectionAppSelectorComponent { val controller: MediaProjectionAppSelectorController val recentsViewController: MediaProjectionRecentsViewController + @get:HostUserHandle val hostUserHandle: UserHandle + @get:PersonalProfile val personalProfileUserHandle: UserHandle @MediaProjectionAppSelector val configurationController: ConfigurationController } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt index d744a40b60d8..52c7ca3bb3d4 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt @@ -17,24 +17,36 @@ package com.android.systemui.mediaprojection.appselector import android.content.ComponentName +import android.os.UserHandle +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import javax.inject.Inject @MediaProjectionAppSelectorScope -class MediaProjectionAppSelectorController @Inject constructor( +class MediaProjectionAppSelectorController +@Inject +constructor( private val recentTaskListProvider: RecentTaskListProvider, private val view: MediaProjectionAppSelectorView, + private val flags: FeatureFlags, + @HostUserHandle private val hostUserHandle: UserHandle, @MediaProjectionAppSelector private val scope: CoroutineScope, @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName ) { fun init() { scope.launch { - val tasks = recentTaskListProvider.loadRecentTasks().sortTasks() + val recentTasks = recentTaskListProvider.loadRecentTasks() + + val tasks = recentTasks + .filterDevicePolicyRestrictedTasks() + .sortedTasks() + view.bind(tasks) } } @@ -43,9 +55,20 @@ class MediaProjectionAppSelectorController @Inject constructor( scope.cancel() } - private fun List<RecentTask>.sortTasks(): List<RecentTask> = - sortedBy { - // Show normal tasks first and only then tasks with opened app selector - it.topActivityComponent == appSelectorComponentName + /** + * Removes all recent tasks that are different from the profile of the host app to avoid any + * cross-profile sharing + */ + private fun List<RecentTask>.filterDevicePolicyRestrictedTasks(): List<RecentTask> = + if (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) { + // TODO(b/263950746): filter tasks based on the enterprise policies + this + } else { + filter { UserHandle.of(it.userId) == hostUserHandle } } + + private fun List<RecentTask>.sortedTasks(): List<RecentTask> = sortedBy { + // Show normal tasks first and only then tasks with opened app selector + it.topActivityComponent == appSelectorComponentName + } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt index cd994b857e95..41e22860d0ad 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -17,11 +17,12 @@ package com.android.systemui.mediaprojection.appselector.data import android.annotation.ColorInt +import android.annotation.UserIdInt import android.content.ComponentName data class RecentTask( val taskId: Int, - val userId: Int, + @UserIdInt val userId: Int, val topActivityComponent: ComponentName?, val baseIntentComponent: ComponentName?, @ColorInt val colorBackground: Int? diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 5b306c98912d..61bb858ee7d7 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; @@ -44,6 +45,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; @@ -136,9 +138,10 @@ import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.rotation.RotationButton; import com.android.systemui.shared.rotation.RotationButtonController; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; @@ -252,6 +255,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final AutoHideController.Factory mAutoHideControllerFactory; private final Optional<TelecomManager> mTelecomManagerOptional; private final InputMethodManager mInputMethodManager; + private final TaskStackChangeListeners mTaskStackChangeListeners; @VisibleForTesting public int mDisplayId; @@ -488,6 +492,18 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; + private boolean mScreenPinningActive = false; + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onLockTaskModeChanged(int mode) { + mScreenPinningActive = (mode == LOCK_TASK_MODE_PINNED); + mSysUiFlagsContainer.setFlag(SYSUI_STATE_SCREEN_PINNING, mScreenPinningActive) + .commitUpdate(mDisplayId); + mView.setInScreenPinning(mScreenPinningActive); + updateScreenPinningGestures(); + } + }; + @Inject NavigationBar( NavigationBarView navigationBarView, @@ -529,7 +545,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements EdgeBackGestureHandler edgeBackGestureHandler, Optional<BackAnimation> backAnimation, UserContextProvider userContextProvider, - WakefulnessLifecycle wakefulnessLifecycle) { + WakefulnessLifecycle wakefulnessLifecycle, + TaskStackChangeListeners taskStackChangeListeners) { super(navigationBarView); mFrame = navigationBarFrame; mContext = context; @@ -568,6 +585,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mInputMethodManager = inputMethodManager; mUserContextProvider = userContextProvider; mWakefulnessLifecycle = wakefulnessLifecycle; + mTaskStackChangeListeners = taskStackChangeListeners; mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); @@ -676,6 +694,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mCommandQueue.recomputeDisableFlags(mDisplayId, false); mNotificationShadeDepthController.addListener(mDepthListener); + mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener); } public void destroyView() { @@ -689,6 +708,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mNotificationShadeDepthController.removeListener(mDepthListener); mDeviceConfigProxy.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); + mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener); } @Override @@ -990,11 +1010,15 @@ public class NavigationBar extends ViewController<NavigationBarView> implements pw.println(" mTransientShown=" + mTransientShown); pw.println(" mTransientShownFromGestureOnSystemBar=" + mTransientShownFromGestureOnSystemBar); + pw.println(" mScreenPinningActive=" + mScreenPinningActive); dumpBarTransitions(pw, "mNavigationBarView", getBarTransitions()); pw.println(" mOrientedHandleSamplingRegion: " + mOrientedHandleSamplingRegion); mView.dump(pw); mRegionSamplingHelper.dump(pw); + if (mAutoHideController != null) { + mAutoHideController.dump(pw); + } } // ----- CommandQueue Callbacks ----- @@ -1213,10 +1237,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private void updateScreenPinningGestures() { // Change the cancel pin gesture to home and back if recents button is invisible - boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); ButtonDispatcher backButton = mView.getBackButton(); ButtonDispatcher recentsButton = mView.getRecentsButton(); - if (pinningActive) { + if (mScreenPinningActive) { boolean recentsVisible = mView.isRecentsButtonVisible(); backButton.setOnLongClickListener(recentsVisible ? this::onLongPressBackRecents @@ -1227,8 +1250,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements recentsButton.setOnLongClickListener(null); } // Note, this needs to be set after even if we're setting the listener to null - backButton.setLongClickable(pinningActive); - recentsButton.setLongClickable(pinningActive); + backButton.setLongClickable(mScreenPinningActive); + recentsButton.setLongClickable(mScreenPinningActive); } private void notifyNavigationBarScreenOn() { @@ -1311,8 +1334,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements @VisibleForTesting boolean onHomeLongClick(View v) { - if (!mView.isRecentsButtonVisible() - && ActivityManagerWrapper.getInstance().isScreenPinningActive()) { + if (!mView.isRecentsButtonVisible() && mScreenPinningActive) { return onLongPressBackHome(v); } if (shouldDisableNavbarGestures()) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 891455249867..5d43c5dc19fa 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -57,6 +57,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.phone.AutoHideController; @@ -88,7 +89,6 @@ public class NavigationBarController implements private FeatureFlags mFeatureFlags; private final DisplayManager mDisplayManager; private final TaskbarDelegate mTaskbarDelegate; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private int mNavMode; @VisibleForTesting boolean mIsTablet; @@ -112,10 +112,10 @@ public class NavigationBarController implements NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, NavigationBarComponent.Factory navigationBarComponentFactory, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, DumpManager dumpManager, AutoHideController autoHideController, LightBarController lightBarController, + TaskStackChangeListeners taskStackChangeListeners, Optional<Pip> pipOptional, Optional<BackAnimation> backAnimation, FeatureFlags featureFlags) { @@ -129,11 +129,10 @@ public class NavigationBarController implements mConfigChanges.applyNewConfig(mContext.getResources()); mNavMode = navigationModeController.addListener(this); mTaskbarDelegate = taskbarDelegate; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService, navBarHelper, navigationModeController, sysUiFlagsContainer, dumpManager, autoHideController, lightBarController, pipOptional, - backAnimation.orElse(null)); + backAnimation.orElse(null), taskStackChangeListeners); mIsTablet = isTablet(mContext); dumpManager.registerDumpable(this); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 403d276f8cbc..88c4fd524b79 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -80,6 +81,7 @@ import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdates import com.android.systemui.shared.rotation.RotationButtonController; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.LightBarTransitionsController; @@ -160,6 +162,7 @@ public class NavigationBarView extends FrameLayout { * fully locked mode we only show that unlocking is blocked. */ private ScreenPinningNotify mScreenPinningNotify; + private boolean mScreenPinningActive = false; /** * {@code true} if the IME can render the back button and the IME switcher button. @@ -636,14 +639,13 @@ public class NavigationBarView extends FrameLayout { // When screen pinning, don't hide back and home when connected service or back and // recents buttons when disconnected from launcher service in screen pinning mode, // as they are used for exiting. - final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); if (mOverviewProxyEnabled) { // Force disable recents when not in legacy mode disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode); - if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) { + if (mScreenPinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) { disableBack = disableHome = false; } - } else if (pinningActive) { + } else if (mScreenPinningActive) { disableBack = disableRecent = false; } @@ -738,9 +740,7 @@ public class NavigationBarView extends FrameLayout { public void updateDisabledSystemUiStateFlags(SysUiState sysUiState) { int displayId = mContext.getDisplayId(); - sysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, - ActivityManagerWrapper.getInstance().isScreenPinningActive()) - .setFlag(SYSUI_STATE_OVERVIEW_DISABLED, + sysUiState.setFlag(SYSUI_STATE_OVERVIEW_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0) .setFlag(SYSUI_STATE_HOME_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0) @@ -749,6 +749,10 @@ public class NavigationBarView extends FrameLayout { .commitUpdate(displayId); } + public void setInScreenPinning(boolean active) { + mScreenPinningActive = active; + } + private void updatePanelSystemUiStateFlags() { if (SysUiState.DEBUG) { Log.d(TAG, "Updating panel sysui state flags: panelView=" + mPanelView); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 5e26e6050eaa..6ee86aa021a3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; @@ -40,6 +41,7 @@ import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode import android.app.StatusBarManager; import android.app.StatusBarManager.WindowVisibleState; +import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -68,6 +70,8 @@ import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; @@ -101,6 +105,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private AutoHideController mAutoHideController; private LightBarController mLightBarController; private LightBarTransitionsController mLightBarTransitionsController; + private TaskStackChangeListeners mTaskStackChangeListeners; private Optional<Pip> mPipOptional; private int mDisplayId; private int mNavigationIconHints; @@ -127,6 +132,14 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private final DisplayManager mDisplayManager; private Context mWindowContext; private ScreenPinningNotify mScreenPinningNotify; + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onLockTaskModeChanged(int mode) { + mSysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, mode == LOCK_TASK_MODE_PINNED) + .commitUpdate(mDisplayId); + } + }; + private int mNavigationMode = -1; private final Consumer<Rect> mPipListener; @@ -176,7 +189,8 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, AutoHideController autoHideController, LightBarController lightBarController, Optional<Pip> pipOptional, - BackAnimation backAnimation) { + BackAnimation backAnimation, + TaskStackChangeListeners taskStackChangeListeners) { // TODO: adding this in the ctor results in a dagger dependency cycle :( mCommandQueue = commandQueue; mOverviewProxyService = overviewProxyService; @@ -189,6 +203,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mPipOptional = pipOptional; mBackAnimation = backAnimation; mLightBarTransitionsController = createLightBarTransitionsController(); + mTaskStackChangeListeners = taskStackChangeListeners; } // Separated into a method to keep setDependencies() clean/readable. @@ -234,6 +249,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener); mEdgeBackGestureHandler.setBackAnimation(mBackAnimation); mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration()); + mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener); mInitialized = true; } @@ -253,6 +269,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mLightBarTransitionsController.destroy(); mLightBarController.setNavigationBar(null); mPipOptional.ifPresent(this::removePipExclusionBoundsChangeListener); + mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener); mInitialized = false; } @@ -300,8 +317,6 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible()) .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, allowSystemGestureIgnoringBarVisibility()) - .setFlag(SYSUI_STATE_SCREEN_PINNING, - ActivityManagerWrapper.getInstance().isScreenPinningActive()) .setFlag(SYSUI_STATE_IMMERSIVE_MODE, isImmersiveMode()) .commitUpdate(mDisplayId); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 13c5b48906c5..eea0e4cd94b9 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -273,7 +273,6 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack @Override public void triggerBack() { // Notify FalsingManager that an intentional gesture has occurred. - // TODO(b/186519446): use a different method than isFalseTouch mFalsingManager.isFalseTouch(BACK_GESTURE); // Only inject back keycodes when ahead-of-time back dispatching is disabled. if (mBackAnimation == null) { @@ -502,6 +501,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void updateIsEnabled() { + try { + Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled"); + updateIsEnabledTraced(); + } finally { + Trace.endSection(); + } + } + + private void updateIsEnabledTraced() { boolean isEnabled = mIsAttached && mIsGesturalModeEnabled; if (isEnabled == mIsEnabled) { return; @@ -587,13 +595,18 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) { - if (mEdgeBackPlugin != null) { - mEdgeBackPlugin.onDestroy(); + try { + Trace.beginSection("setEdgeBackPlugin"); + if (mEdgeBackPlugin != null) { + mEdgeBackPlugin.onDestroy(); + } + mEdgeBackPlugin = edgeBackPlugin; + mEdgeBackPlugin.setBackCallback(mBackCallback); + mEdgeBackPlugin.setLayoutParams(createLayoutParams()); + updateDisplaySize(); + } finally { + Trace.endSection(); } - mEdgeBackPlugin = edgeBackPlugin; - mEdgeBackPlugin.setBackCallback(mBackCallback); - mEdgeBackPlugin.setLayoutParams(createLayoutParams()); - updateDisplaySize(); } public boolean isHandlingGestures() { @@ -919,6 +932,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mThresholdCrossed = true; // Capture inputs mInputMonitor.pilferPointers(); + if (mBackAnimation != null) { + // Notify FalsingManager that an intentional gesture has occurred. + mFalsingManager.isFalseTouch(BACK_GESTURE); + } mInputEventReceiver.setBatchingEnabled(true); } else { logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_FAR_FROM_EDGE); diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 6dd60d043a06..08d18575da79 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -57,7 +57,9 @@ constructor( * If the keyguard is locked, notes will open as a full screen experience. A locked device has * no contextual information which let us use the whole screen space available. * - * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * If no in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be + * collapsed if the notes bubble is already opened. + * * That will let users open other apps in full screen, and take contextual notes. */ fun showNoteTask(isInMultiWindowMode: Boolean = false) { @@ -75,7 +77,7 @@ constructor( context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. - bubbles.showAppBubble(intent) + bubbles.showOrHideAppBubble(intent) } } @@ -102,4 +104,9 @@ constructor( PackageManager.DONT_KILL_APP, ) } + + companion object { + // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead. + const val NOTE_TASK_KEY_EVENT = 311 + } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index d14b7a766762..d5f4a5a5d351 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -16,7 +16,6 @@ package com.android.systemui.notetask -import android.view.KeyEvent import androidx.annotation.VisibleForTesting import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles @@ -37,7 +36,7 @@ constructor( val callbacks = object : CommandQueue.Callbacks { override fun handleSystemKey(keyCode: Int) { - if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) { noteTaskController.showNoteTask() } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt index 98d69910aac3..26e3f49828c7 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -21,12 +21,12 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import javax.inject.Inject /** - * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an - * [Intent] ready for be launched will be returned. Otherwise, returns null. + * Class responsible to query all apps and find one that can handle the [ACTION_CREATE_NOTE]. If + * found, an [Intent] ready for be launched will be returned. Otherwise, returns null. * * TODO(b/248274123): should be revisited once the notes role is implemented. */ @@ -37,15 +37,16 @@ constructor( ) { fun resolveIntent(): Intent? { - val intent = Intent(NOTES_ACTION) + val intent = Intent(ACTION_CREATE_NOTE) val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) val infoList = packageManager.queryIntentActivities(intent, flags) for (info in infoList) { - val packageName = info.serviceInfo.applicationInfo.packageName ?: continue + val packageName = info.activityInfo.applicationInfo.packageName ?: continue val activityName = resolveActivityNameForNotesAction(packageName) ?: continue - return Intent(NOTES_ACTION) + return Intent(ACTION_CREATE_NOTE) + .setPackage(packageName) .setComponent(ComponentName(packageName, activityName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } @@ -54,7 +55,7 @@ constructor( } private fun resolveActivityNameForNotesAction(packageName: String): String? { - val intent = Intent(NOTES_ACTION).setPackage(packageName) + val intent = Intent(ACTION_CREATE_NOTE).setPackage(packageName) val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) val resolveInfo = packageManager.resolveActivity(intent, flags) @@ -69,8 +70,8 @@ constructor( } companion object { - // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead. - const val NOTES_ACTION = "android.intent.action.NOTES" + // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. + const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE" } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 47fe67638cd0..f203e7a51643 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -45,8 +45,8 @@ constructor( fun newIntent(context: Context): Intent { return Intent(context, LaunchNoteTaskActivity::class.java).apply { // Intent's action must be set in shortcuts, or an exception will be thrown. - // TODO(b/254606432): Use Intent.ACTION_NOTES instead. - action = NoteTaskIntentResolver.NOTES_ACTION + // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. + action = NoteTaskIntentResolver.ACTION_CREATE_NOTE } } } diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index fba5f63ea9c7..7f0f89415280 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -68,8 +68,10 @@ public class PeopleSpaceActivity extends ComponentActivity { }; if (ComposeFacade.INSTANCE.isComposeAvailable()) { + Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity"); ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult); } else { + Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity"); ViewGroup view = PeopleViewBinder.create(this); PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult); setContentView(view); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index f49ffb4d930c..8ad102ece9b3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -49,6 +49,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; +import com.android.systemui.compose.ComposeFacade; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.controls.ui.MediaHost; @@ -68,6 +69,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import java.io.PrintWriter; import java.util.Arrays; @@ -227,9 +229,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */ this); - LinearLayout footerActionsView = view.findViewById(R.id.qs_footer_actions); - FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel, - mListeningAndVisibilityLifecycleOwner); + bindFooterActionsView(view); mFooterActionsController.init(); mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); @@ -290,6 +290,33 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca }); } + private void bindFooterActionsView(View root) { + LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions); + + if (!ComposeFacade.INSTANCE.isComposeAvailable()) { + Log.d(TAG, "Binding the View implementation of the QS footer actions"); + FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel, + mListeningAndVisibilityLifecycleOwner); + return; + } + + // Compose is available, so let's use the Compose implementation of the footer actions. + Log.d(TAG, "Binding the Compose implementation of the QS footer actions"); + View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(), + mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner); + + // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin + // to all views except for qs_footer_actions, so we set it to the Compose view. + composeView.setId(R.id.qs_footer_actions); + + // Replace the View by the Compose provided one. + ViewGroup parent = (ViewGroup) footerActionsView.getParent(); + ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams(); + int index = parent.indexOfChild(footerActionsView); + parent.removeViewAt(index); + parent.addView(composeView, index, layoutParams); + } + @Override public void setScrollListener(ScrollListener listener) { mScrollListener = listener; @@ -683,7 +710,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } else { mQsMediaHost.setSquishFraction(mSquishinessFraction); } - + updateMediaPositions(); } private void setAlphaAnimationProgress(float progress) { @@ -758,6 +785,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca - mQSPanelController.getPaddingBottom()); } + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + View hostView = mQsMediaHost.getHostView(); + // Make sure the media appears a bit from the top to make it look nicer + if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() + && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { + float interpolation = 1.0f - mLastQSExpansion; + interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); + float translationY = -hostView.getHeight() * 1.3f * interpolation; + hostView.setTranslationY(translationY); + } else { + hostView.setTranslationY(0); + } + } + } + private boolean headerWillBeAnimating() { return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 7cf63f678c1d..1da30ade951b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -36,7 +36,6 @@ public interface QSHost { void removeCallback(Callback callback); void removeTile(String tileSpec); void removeTiles(Collection<String> specs); - void unmarkTileAsAutoAdded(String tileSpec); int indexOf(String tileSpec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 7bb672ce5880..e85d0a32cfa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -372,18 +372,18 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr if (mUsingHorizontalLayout) { // Only height remaining parameters.getDisappearSize().set(0.0f, 0.4f); - // Disappearing on the right side on the bottom - parameters.getGonePivot().set(1.0f, 1.0f); + // Disappearing on the right side on the top + parameters.getGonePivot().set(1.0f, 0.0f); // translating a bit horizontal parameters.getContentTranslationFraction().set(0.25f, 1.0f); parameters.setDisappearEnd(0.6f); } else { // Only width remaining parameters.getDisappearSize().set(1.0f, 0.0f); - // Disappearing on the bottom - parameters.getGonePivot().set(0.0f, 1.0f); + // Disappearing on the top + parameters.getGonePivot().set(0.0f, 0.0f); // translating a bit vertical - parameters.getContentTranslationFraction().set(0.0f, 1.05f); + parameters.getContentTranslationFraction().set(0.0f, 1f); parameters.setDisappearEnd(0.95f); } parameters.setFadeStartPosition(0.95f); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index cad296b671b3..100853caa2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -427,11 +427,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs))); } - @Override - public void unmarkTileAsAutoAdded(String spec) { - if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); - } - /** * Add a tile to the end * diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index 79fcc7d81372..17124901e4de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -24,6 +24,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.Toolbar; @@ -74,8 +75,8 @@ public class QSCustomizer extends LinearLayout { toolbar.setNavigationIcon( getResources().getDrawable(value.resourceId, mContext.getTheme())); - toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, - mContext.getString(com.android.internal.R.string.reset)); + toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); toolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 3d48fd109e39..84a18d8dd365 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -132,7 +132,7 @@ public class TileServices extends IQSService.Stub { final String slot = tile.getComponent().getClassName(); // TileServices doesn't know how to add more than 1 icon per slot, so remove all mMainHandler.post(() -> mHost.getIconController() - .removeAllIconsForSlot(slot)); + .removeAllIconsForExternalSlot(slot)); } } 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 30f81243e8d0..19215867e678 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 @@ -219,9 +219,9 @@ object FooterActionsViewBinder { // Small button with the number only. foregroundServicesWithTextView.isVisible = false - foregroundServicesWithNumberView.visibility = View.VISIBLE + foregroundServicesWithNumberView.isVisible = true foregroundServicesWithNumberView.setOnClickListener { - foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView)) } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 9f376ae75efe..d32ef327e90e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -49,109 +49,135 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : } fun logTileAdded(tileSpec: String) { - log(DEBUG, { - str1 = tileSpec - }, { - "[$str1] Tile added" - }) + buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" }) } fun logTileDestroyed(tileSpec: String, reason: String) { - log(DEBUG, { - str1 = tileSpec - str2 = reason - }, { - "[$str1] Tile destroyed. Reason: $str2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + str2 = reason + }, + { "[$str1] Tile destroyed. Reason: $str2" } + ) } fun logTileChangeListening(tileSpec: String, listening: Boolean) { - log(VERBOSE, { - bool1 = listening - str1 = tileSpec - }, { - "[$str1] Tile listening=$bool1" - }) + buffer.log( + TAG, + VERBOSE, + { + bool1 = listening + str1 = tileSpec + }, + { "[$str1] Tile listening=$bool1" } + ) } fun logAllTilesChangeListening(listening: Boolean, containerName: String, allSpecs: String) { - log(DEBUG, { - bool1 = listening - str1 = containerName - str2 = allSpecs - }, { - "Tiles listening=$bool1 in $str1. $str2" - }) + buffer.log( + TAG, + DEBUG, + { + bool1 = listening + str1 = containerName + str2 = allSpecs + }, + { "Tiles listening=$bool1 in $str1. $str2" } + ) } fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling click." } + ) } fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleSecondaryClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling secondary click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling secondary click." } + ) } fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleLongClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling long click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling long click." } + ) } fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) { - log(VERBOSE, { - str1 = tileSpec - int1 = lastType - str2 = callback - }, { - "[$str1] mLastTileState=$int1, Callback=$str2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = lastType + str2 = callback + }, + { "[$str1] mLastTileState=$int1, Callback=$str2." } + ) } // TODO(b/250618218): Remove this method once we know the root cause of b/250618218. @@ -167,58 +193,75 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : if (tileSpec != "internet") { return } - log(VERBOSE, { - str1 = tileSpec - int1 = state - bool1 = disabledByPolicy - int2 = color - }, { - "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = state + bool1 = disabledByPolicy + int2 = color + }, + { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." } + ) } fun logTileUpdated(tileSpec: String, state: QSTile.State) { - log(VERBOSE, { - str1 = tileSpec - str2 = state.label?.toString() - str3 = state.icon?.toString() - int1 = state.state - if (state is QSTile.SignalState) { - bool1 = true - bool2 = state.activityIn - bool3 = state.activityOut + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + str2 = state.label?.toString() + str3 = state.icon?.toString() + int1 = state.state + if (state is QSTile.SignalState) { + bool1 = true + bool2 = state.activityIn + bool3 = state.activityOut + } + }, + { + "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + + if (bool1) " Activity in/out=$bool2/$bool3" else "" } - }, { - "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + - if (bool1) " Activity in/out=$bool2/$bool3" else "" - }) + ) } fun logPanelExpanded(expanded: Boolean, containerName: String) { - log(DEBUG, { - str1 = containerName - bool1 = expanded - }, { - "$str1 expanded=$bool1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = expanded + }, + { "$str1 expanded=$bool1" } + ) } fun logOnViewAttached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewAttached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewAttached: $str1 orientation $int1" } + ) } fun logOnViewDetached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewDetached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewDetached: $str1 orientation $int1" } + ) } fun logOnConfigurationChanged( @@ -226,13 +269,16 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : newOrientation: Int, containerName: String ) { - log(DEBUG, { - str1 = containerName - int1 = lastOrientation - int2 = newOrientation - }, { - "configuration change: $str1 orientation was $int1, now $int2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = lastOrientation + int2 = newOrientation + }, + { "configuration change: $str1 orientation was $int1, now $int2" } + ) } fun logSwitchTileLayout( @@ -241,32 +287,41 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : force: Boolean, containerName: String ) { - log(DEBUG, { - str1 = containerName - bool1 = after - bool2 = before - bool3 = force - }, { - "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = after + bool2 = before + bool3 = force + }, + { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" } + ) } fun logTileDistributionInProgress(tilesPerPageCount: Int, totalTilesCount: Int) { - log(DEBUG, { - int1 = tilesPerPageCount - int2 = totalTilesCount - }, { - "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" - }) + buffer.log( + TAG, + DEBUG, + { + int1 = tilesPerPageCount + int2 = totalTilesCount + }, + { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" } + ) } fun logTileDistributed(tileName: String, pageIndex: Int) { - log(DEBUG, { - str1 = tileName - int1 = pageIndex - }, { - "Adding $str1 to page number $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileName + int1 = pageIndex + }, + { "Adding $str1 to page number $int1" } + ) } private fun toStateString(state: Int): String { @@ -277,12 +332,4 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : else -> "wrong state" } } - - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index a92c7e3c8554..24a4f60b7c00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -33,7 +33,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile; import com.android.systemui.qs.tiles.BluetoothTile; import com.android.systemui.qs.tiles.CameraToggleTile; import com.android.systemui.qs.tiles.CastTile; -import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorCorrectionTile; import com.android.systemui.qs.tiles.ColorInversionTile; import com.android.systemui.qs.tiles.DataSaverTile; @@ -54,7 +53,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.qs.tiles.ScreenRecordTile; import com.android.systemui.qs.tiles.UiModeNightTile; -import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; import com.android.systemui.util.leak.GarbageMonitor; @@ -68,10 +66,8 @@ public class QSFactoryImpl implements QSFactory { private static final String TAG = "QSFactory"; - private final Provider<WifiTile> mWifiTileProvider; private final Provider<InternetTile> mInternetTileProvider; private final Provider<BluetoothTile> mBluetoothTileProvider; - private final Provider<CellularTile> mCellularTileProvider; private final Provider<DndTile> mDndTileProvider; private final Provider<ColorCorrectionTile> mColorCorrectionTileProvider; private final Provider<ColorInversionTile> mColorInversionTileProvider; @@ -106,10 +102,8 @@ public class QSFactoryImpl implements QSFactory { public QSFactoryImpl( Lazy<QSHost> qsHostLazy, Provider<CustomTile.Builder> customTileBuilderProvider, - Provider<WifiTile> wifiTileProvider, Provider<InternetTile> internetTileProvider, Provider<BluetoothTile> bluetoothTileProvider, - Provider<CellularTile> cellularTileProvider, Provider<DndTile> dndTileProvider, Provider<ColorInversionTile> colorInversionTileProvider, Provider<AirplaneModeTile> airplaneModeTileProvider, @@ -139,10 +133,8 @@ public class QSFactoryImpl implements QSFactory { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; - mWifiTileProvider = wifiTileProvider; mInternetTileProvider = internetTileProvider; mBluetoothTileProvider = bluetoothTileProvider; - mCellularTileProvider = cellularTileProvider; mDndTileProvider = dndTileProvider; mColorInversionTileProvider = colorInversionTileProvider; mAirplaneModeTileProvider = airplaneModeTileProvider; @@ -186,14 +178,10 @@ public class QSFactoryImpl implements QSFactory { protected QSTileImpl createTileInternal(String tileSpec) { // Stock tiles. switch (tileSpec) { - case "wifi": - return mWifiTileProvider.get(); case "internet": return mInternetTileProvider.get(); case "bt": return mBluetoothTileProvider.get(); - case "cell": - return mCellularTileProvider.get(); case "dnd": return mDndTileProvider.get(); case "inversion": diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index b355d4bb67fe..29d7fb02e613 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -145,7 +145,6 @@ open class QSTileViewImpl @JvmOverloads constructor( private val launchableViewDelegate = LaunchableViewDelegate( this, superSetVisibility = { super.setVisibility(it) }, - superSetTransitionVisibility = { super.setTransitionVisibility(it) }, ) private var lastDisabledByPolicy = false @@ -362,10 +361,6 @@ open class QSTileViewImpl @JvmOverloads constructor( launchableViewDelegate.setVisibility(visibility) } - override fun setTransitionVisibility(visibility: Int) { - launchableViewDelegate.setTransitionVisibility(visibility) - } - // Accessibility override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java deleted file mode 100644 index 04a25fc193b3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.tiles; - -import static com.android.systemui.Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA; - -import android.annotation.NonNull; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.telephony.SubscriptionManager; -import android.text.Html; -import android.text.TextUtils; -import android.view.View; -import android.view.WindowManager.LayoutParams; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settingslib.net.DataUsageController; -import com.android.systemui.Prefs; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.SignalTileView; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.IconState; -import com.android.systemui.statusbar.connectivity.MobileDataIndicators; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.policy.KeyguardStateController; - -import javax.inject.Inject; - -/** Quick settings tile: Cellular **/ -public class CellularTile extends QSTileImpl<SignalState> { - private static final String ENABLE_SETTINGS_DATA_PLAN = "enable.settings.data.plan"; - - private final NetworkController mController; - private final DataUsageController mDataController; - private final KeyguardStateController mKeyguard; - private final CellSignalCallback mSignalCallback = new CellSignalCallback(); - - @Inject - public CellularTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - KeyguardStateController keyguardStateController - - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mKeyguard = keyguardStateController; - mDataController = mController.getMobileDataController(); - mController.observe(getLifecycle(), mSignalCallback); - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new SignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return new Intent(Settings.ACTION_WIRELESS_SETTINGS); - } - return getCellularSettingIntent(); - } - - @Override - protected void handleClick(@Nullable View view) { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return; - } - if (mDataController.isMobileDataEnabled()) { - maybeShowDisableDialog(); - } else { - mDataController.setMobileDataEnabled(true); - } - } - - private void maybeShowDisableDialog() { - if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) { - // Directly turn off mobile data if the user has seen the dialog before. - mDataController.setMobileDataEnabled(false); - return; - } - String carrierName = mController.getMobileDataNetworkName(); - boolean isInService = mController.isMobileDataNetworkInService(); - if (TextUtils.isEmpty(carrierName) || !isInService) { - carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier); - } - AlertDialog dialog = new Builder(mContext) - .setTitle(R.string.mobile_data_disable_title) - .setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton( - com.android.internal.R.string.alert_windows_notification_turn_off_action, - (d, w) -> { - mDataController.setMobileDataEnabled(false); - Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true); - }) - .create(); - dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG); - SystemUIDialog.setShowForAllUsers(dialog, true); - SystemUIDialog.registerDismissListener(dialog); - SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing()); - dialog.show(); - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - handleLongClick(view); - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_cellular_detail_title); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - CallbackInfo cb = (CallbackInfo) arg; - if (cb == null) { - cb = mSignalCallback.mInfo; - } - - final Resources r = mContext.getResources(); - state.label = r.getString(R.string.mobile_data); - boolean mobileDataEnabled = mDataController.isMobileDataSupported() - && mDataController.isMobileDataEnabled(); - state.value = mobileDataEnabled; - state.activityIn = mobileDataEnabled && cb.activityIn; - state.activityOut = mobileDataEnabled && cb.activityOut; - state.expandedAccessibilityClassName = Switch.class.getName(); - if (cb.noSim) { - state.icon = ResourceIcon.get(R.drawable.ic_qs_no_sim); - } else { - state.icon = ResourceIcon.get(R.drawable.ic_swap_vert); - } - - if (cb.noSim) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.keyguard_missing_sim_message_short); - } else if (cb.airplaneModeEnabled) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.status_bar_airplane); - } else if (mobileDataEnabled) { - state.state = Tile.STATE_ACTIVE; - state.secondaryLabel = appendMobileDataType( - // Only show carrier name if there are more than 1 subscription - cb.multipleSubs ? cb.dataSubscriptionName : "", - getMobileDataContentName(cb)); - } else { - state.state = Tile.STATE_INACTIVE; - state.secondaryLabel = r.getString(R.string.cell_data_off); - } - - state.contentDescription = state.label; - if (state.state == Tile.STATE_INACTIVE) { - // This information is appended later by converting the Tile.STATE_INACTIVE state. - state.stateDescription = ""; - } else { - state.stateDescription = state.secondaryLabel; - } - } - - private CharSequence appendMobileDataType(CharSequence current, CharSequence dataType) { - if (TextUtils.isEmpty(dataType)) { - return Html.fromHtml(current.toString(), 0); - } - if (TextUtils.isEmpty(current)) { - return Html.fromHtml(dataType.toString(), 0); - } - String concat = mContext.getString(R.string.mobile_carrier_text_format, current, dataType); - return Html.fromHtml(concat, 0); - } - - private CharSequence getMobileDataContentName(CallbackInfo cb) { - if (cb.roaming && !TextUtils.isEmpty(cb.dataContentDescription)) { - String roaming = mContext.getString(R.string.data_connection_roaming); - String dataDescription = cb.dataContentDescription.toString(); - return mContext.getString(R.string.mobile_data_text_format, roaming, dataDescription); - } - if (cb.roaming) { - return mContext.getString(R.string.data_connection_roaming); - } - return cb.dataContentDescription; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_CELLULAR; - } - - @Override - public boolean isAvailable() { - return mController.hasMobileDataFeature() - && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM; - } - - private static final class CallbackInfo { - boolean airplaneModeEnabled; - @Nullable - CharSequence dataSubscriptionName; - @Nullable - CharSequence dataContentDescription; - boolean activityIn; - boolean activityOut; - boolean noSim; - boolean roaming; - boolean multipleSubs; - } - - private final class CellSignalCallback implements SignalCallback { - private final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { - if (indicators.qsIcon == null) { - // Not data sim, don't display. - return; - } - mInfo.dataSubscriptionName = mController.getMobileDataNetworkName(); - mInfo.dataContentDescription = indicators.qsDescription != null - ? indicators.typeContentDescriptionHtml : null; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.roaming = indicators.roaming; - mInfo.multipleSubs = mController.getNumberSubscriptions() > 1; - refreshState(mInfo); - } - - @Override - public void setNoSims(boolean show, boolean simDetected) { - mInfo.noSim = show; - refreshState(mInfo); - } - - @Override - public void setIsAirplaneMode(@NonNull IconState icon) { - mInfo.airplaneModeEnabled = icon.visible; - refreshState(mInfo); - } - } - - static Intent getCellularSettingIntent() { - Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); - int dataSub = SubscriptionManager.getDefaultDataSubscriptionId(); - if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - intent.putExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.getDefaultDataSubscriptionId()); - } - return intent; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 57a00c9a1620..b6b657ec82f6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -204,15 +204,6 @@ public class UserDetailView extends PseudoGridView { Trace.endSection(); } - @Override - public void onUserListItemClicked(@NonNull UserRecord record, - @Nullable UserSwitchDialogController.DialogShower dialogShower) { - if (dialogShower != null) { - mDialogShower.dismiss(); - } - super.onUserListItemClicked(record, dialogShower); - } - public void linkToViewGroup(ViewGroup viewGroup) { PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java deleted file mode 100644 index b2be56cca51e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.tiles; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.AlphaControlledSignalTileView; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSIconViewImpl; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.AccessPointController; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.connectivity.WifiIcons; -import com.android.systemui.statusbar.connectivity.WifiIndicators; - -import javax.inject.Inject; - -/** Quick settings tile: Wifi **/ -public class WifiTile extends QSTileImpl<SignalState> { - private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS); - - protected final NetworkController mController; - private final AccessPointController mWifiController; - private final QSTile.SignalState mStateBeforeClick = newTileState(); - - protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback(); - private boolean mExpectDisabled; - - @Inject - public WifiTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - AccessPointController accessPointController - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mWifiController = accessPointController; - mController.observe(getLifecycle(), mSignalCallback); - mStateBeforeClick.spec = "wifi"; - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new AlphaControlledSignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - return WIFI_SETTINGS; - } - - @Override - protected void handleClick(@Nullable View view) { - // Secondary clicks are header clicks, just toggle. - mState.copyTo(mStateBeforeClick); - boolean wifiEnabled = mState.value; - // Immediately enter transient state when turning on wifi. - refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING); - mController.setWifiEnabled(!wifiEnabled); - mExpectDisabled = wifiEnabled; - if (mExpectDisabled) { - mHandler.postDelayed(() -> { - if (mExpectDisabled) { - mExpectDisabled = false; - refreshState(); - } - }, QSIconViewImpl.QS_ANIM_LENGTH); - } - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - if (!mWifiController.canConfigWifi()) { - mActivityStarter.postStartActivityDismissingKeyguard( - new Intent(Settings.ACTION_WIFI_SETTINGS), 0); - return; - } - if (!mState.value) { - mController.setWifiEnabled(true); - } - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_wifi_label); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg); - final CallbackInfo cb = mSignalCallback.mInfo; - if (mExpectDisabled) { - if (cb.enabled) { - return; // Ignore updates until disabled event occurs. - } else { - mExpectDisabled = false; - } - } - boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING; - boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) - && (cb.ssid != null || cb.wifiSignalIconId != WifiIcons.QS_WIFI_NO_NETWORK); - boolean wifiNotConnected = (cb.ssid == null) - && (cb.wifiSignalIconId == WifiIcons.QS_WIFI_NO_NETWORK); - if (state.slash == null) { - state.slash = new SlashState(); - state.slash.rotation = 6; - } - state.slash.isSlashed = false; - boolean isTransient = transientEnabling || cb.isTransient; - state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel); - state.state = Tile.STATE_ACTIVE; - state.dualTarget = true; - state.value = transientEnabling || cb.enabled; - state.activityIn = cb.enabled && cb.activityIn; - state.activityOut = cb.enabled && cb.activityOut; - final StringBuffer minimalContentDescription = new StringBuffer(); - final StringBuffer minimalStateDescription = new StringBuffer(); - final Resources r = mContext.getResources(); - if (isTransient) { - state.icon = ResourceIcon.get( - com.android.internal.R.drawable.ic_signal_wifi_transient_animation); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (!state.value) { - state.slash.isSlashed = true; - state.state = Tile.STATE_INACTIVE; - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_DISABLED); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (wifiConnected) { - state.icon = ResourceIcon.get(cb.wifiSignalIconId); - state.label = cb.ssid != null ? removeDoubleQuotes(cb.ssid) : getTileLabel(); - } else if (wifiNotConnected) { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } - minimalContentDescription.append( - mContext.getString(R.string.quick_settings_wifi_label)).append(","); - if (state.value) { - if (wifiConnected) { - minimalStateDescription.append(cb.wifiSignalContentDescription); - minimalContentDescription.append(removeDoubleQuotes(cb.ssid)); - if (!TextUtils.isEmpty(state.secondaryLabel)) { - minimalContentDescription.append(",").append(state.secondaryLabel); - } - } - } - state.stateDescription = minimalStateDescription.toString(); - state.contentDescription = minimalContentDescription.toString(); - state.dualLabelContentDescription = r.getString( - R.string.accessibility_quick_settings_open_settings, getTileLabel()); - state.expandedAccessibilityClassName = Switch.class.getName(); - } - - private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) { - return isTransient - ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient) - : statusLabel; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_WIFI; - } - - @Override - public boolean isAvailable() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); - } - - @Nullable - private static String removeDoubleQuotes(String string) { - if (string == null) return null; - final int length = string.length(); - if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { - return string.substring(1, length - 1); - } - return string; - } - - protected static final class CallbackInfo { - boolean enabled; - boolean connected; - int wifiSignalIconId; - @Nullable - String ssid; - boolean activityIn; - boolean activityOut; - @Nullable - String wifiSignalContentDescription; - boolean isTransient; - @Nullable - public String statusLabel; - - @Override - public String toString() { - return new StringBuilder("CallbackInfo[") - .append("enabled=").append(enabled) - .append(",connected=").append(connected) - .append(",wifiSignalIconId=").append(wifiSignalIconId) - .append(",ssid=").append(ssid) - .append(",activityIn=").append(activityIn) - .append(",activityOut=").append(activityOut) - .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription) - .append(",isTransient=").append(isTransient) - .append(']').toString(); - } - } - - protected final class WifiSignalCallback implements SignalCallback { - final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setWifiIndicators(@NonNull WifiIndicators indicators) { - if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + indicators.enabled); - if (indicators.qsIcon == null) { - return; - } - mInfo.enabled = indicators.enabled; - mInfo.connected = indicators.qsIcon.visible; - mInfo.wifiSignalIconId = indicators.qsIcon.icon; - mInfo.ssid = indicators.description; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.wifiSignalContentDescription = indicators.qsIcon.contentDescription; - mInfo.isTransient = indicators.isTransient; - mInfo.statusLabel = indicators.statusLabel; - refreshState(); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index a6c7781d891c..72c6bfe371ce 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -101,7 +101,6 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements @MainThread public void onManagedProfileRemoved() { mHost.removeTile(getTileSpec()); - mHost.unmarkTileAsAutoAdded(getTileSpec()); } @Override 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 314252bf310b..4c9c99cc16f0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -36,6 +36,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.QSUserSwitcherEvent import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.ui.dialog.DialogShowerImpl import javax.inject.Inject import javax.inject.Provider @@ -130,19 +131,6 @@ class UserSwitchDialogController @VisibleForTesting constructor( } } - private class DialogShowerImpl( - private val animateFrom: Dialog, - private val dialogLaunchAnimator: DialogLaunchAnimator - ) : DialogInterface by animateFrom, DialogShower { - override fun showDialog(dialog: Dialog, cuj: DialogCuj) { - dialogLaunchAnimator.showFromDialog( - dialog, - animateFrom = animateFrom, - cuj - ) - } - } - interface DialogShower : DialogInterface { fun showDialog(dialog: Dialog, cuj: DialogCuj) } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index 44b18ec4639b..68e3dcdb63ea 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -23,6 +23,7 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.ResultReceiver +import android.os.UserHandle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -77,6 +78,14 @@ class ScreenRecordPermissionDialog( MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER, CaptureTargetResultReceiver() ) + + // Send SystemUI's user handle as the host app user handle because SystemUI + // is the 'host app' (the app that receives screen capture data) + intent.putExtra( + MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, + UserHandle.of(UserHandle.myUserId()) + ) + val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!) if (animationController == null) { dismiss() diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index b4934cf7b804..bf5fbd223186 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -20,8 +20,7 @@ import static com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE; import static com.android.systemui.screenshot.LogConfig.logTag; -import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION; -import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS; +import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType; import android.app.ActivityTaskManager; import android.app.Notification; @@ -155,7 +154,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { CompletableFuture<List<Notification.Action>> smartActionsFuture = mScreenshotSmartActions.getSmartActionsFuture( - mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS, + mScreenshotId, uri, image, mSmartActionsProvider, + ScreenshotSmartActionType.REGULAR_SMART_ACTIONS, smartActionsEnabled, user); List<Notification.Action> smartActions = new ArrayList<>(); if (smartActionsEnabled) { @@ -166,7 +166,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { smartActions.addAll(buildSmartActions( mScreenshotSmartActions.getSmartActions( mScreenshotId, smartActionsFuture, timeoutMs, - mSmartActionsProvider, REGULAR_SMART_ACTIONS), + mSmartActionsProvider, + ScreenshotSmartActionType.REGULAR_SMART_ACTIONS), mContext)); } @@ -476,7 +477,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { CompletableFuture<List<Notification.Action>> quickShareActionsFuture = mScreenshotSmartActions.getSmartActionsFuture( mScreenshotId, null, image, mSmartActionsProvider, - QUICK_SHARE_ACTION, + ScreenshotSmartActionType.QUICK_SHARE_ACTION, true /* smartActionsEnabled */, user); int timeoutMs = DeviceConfig.getInt( DeviceConfig.NAMESPACE_SYSTEMUI, @@ -485,7 +486,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { List<Notification.Action> quickShareActions = mScreenshotSmartActions.getSmartActions( mScreenshotId, quickShareActionsFuture, timeoutMs, - mSmartActionsProvider, QUICK_SHARE_ACTION); + mSmartActionsProvider, + ScreenshotSmartActionType.QUICK_SHARE_ACTION); if (!quickShareActions.isEmpty()) { mQuickShareData.quickShareAction = quickShareActions.get(0); mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 5716a1d7260c..4f94ed18e1ef 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -280,6 +280,7 @@ public class ScreenshotController { private final TimeoutHandler mScreenshotHandler; private final ActionIntentExecutor mActionExecutor; private final UserManager mUserManager; + private final WorkProfileMessageController mWorkProfileMessageController; private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { if (DEBUG_INPUT) { @@ -326,7 +327,8 @@ public class ScreenshotController { BroadcastSender broadcastSender, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ActionIntentExecutor actionExecutor, - UserManager userManager + UserManager userManager, + WorkProfileMessageController workProfileMessageController ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; @@ -358,6 +360,7 @@ public class ScreenshotController { mFlags = flags; mActionExecutor = actionExecutor; mUserManager = userManager; + mWorkProfileMessageController = workProfileMessageController; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -550,6 +553,10 @@ public class ScreenshotController { Log.d(TAG, "adding OnComputeInternalInsetsListener"); } mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView); + if (DEBUG_WINDOW) { + Log.d(TAG, "setContentView: " + mScreenshotView); + } + setContentView(mScreenshotView); } /** @@ -631,6 +638,7 @@ public class ScreenshotController { // The window is focusable by default setWindowFocusable(true); + mScreenshotView.requestFocus(); // Wait until this window is attached to request because it is // the reference used to locate the target window (below). @@ -683,16 +691,12 @@ public class ScreenshotController { return true; } }); - if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mScreenshotView.badgeScreenshot( - mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); + mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( + mContext.getDrawable(R.drawable.overlay_badge_background), owner)); } mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); - if (DEBUG_WINDOW) { - Log.d(TAG, "setContentView: " + mScreenshotView); - } - setContentView(mScreenshotView); + // ignore system bar insets for the purpose of window layout mWindow.getDecorView().setOnApplyWindowInsetsListener( (v, insets) -> WindowInsets.CONSUMED); @@ -784,9 +788,9 @@ public class ScreenshotController { mLongScreenshotHolder.setLongScreenshot(longScreenshot); mLongScreenshotHolder.setTransitionDestinationCallback( (transitionDestination, onTransitionEnd) -> { - mScreenshotView.startLongScreenshotTransition( - transitionDestination, onTransitionEnd, - longScreenshot); + mScreenshotView.startLongScreenshotTransition( + transitionDestination, onTransitionEnd, + longScreenshot); // TODO: Do this via ActionIntentExecutor instead. mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); } @@ -1037,10 +1041,8 @@ public class ScreenshotController { private void doPostAnimation(ScreenshotController.SavedImageData imageData) { mScreenshotView.setChipIntents(imageData); - if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY) - && mUserManager.isManagedProfile(imageData.owner.getIdentifier())) { - // TODO: Read app from configuration - mScreenshotView.showWorkProfileMessage("Files"); + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mWorkProfileMessageController.onScreenshotTaken(imageData.owner, mScreenshotView); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index e8ceb521b6b0..200a7dc08bb3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -33,6 +33,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; @@ -100,7 +101,8 @@ import java.util.ArrayList; * Handles the visual elements and animations for the screenshot flow. */ public class ScreenshotView extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener { + ViewTreeObserver.OnComputeInternalInsetsListener, + WorkProfileMessageController.WorkProfileMessageDisplay { interface ScreenshotViewCallback { void onUserInteraction(); @@ -351,13 +353,23 @@ public class ScreenshotView extends FrameLayout implements * been taken and which app can be used to view it. * * @param appName The name of the app to use to view screenshots + * @param appIcon Optional icon for the relevant files app + * @param onDismiss Runnable to be run when the user dismisses this message */ - void showWorkProfileMessage(String appName) { + @Override + public void showWorkProfileMessage(CharSequence appName, @Nullable Drawable appIcon, + Runnable onDismiss) { + if (appIcon != null) { + // Replace the default icon if one is provided. + ImageView imageView = mMessageContainer.findViewById(R.id.screenshot_message_icon); + imageView.setImageDrawable(appIcon); + } mMessageContent.setText( mContext.getString(R.string.screenshot_work_profile_notification, appName)); mMessageContainer.setVisibility(VISIBLE); mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> { mMessageContainer.setVisibility(View.GONE); + onDismiss.run(); }); } @@ -1078,7 +1090,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotBadge.setVisibility(View.GONE); mScreenshotBadge.setImageDrawable(null); mPendingSharedTransition = false; - mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainerBackground.setVisibility(View.INVISIBLE); mActionsContainer.setVisibility(View.GONE); mDismissButton.setVisibility(View.GONE); mScrollingScrim.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 2176825d8b38..35e9f3e56723 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -21,7 +21,6 @@ import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE; import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI; -import static com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR; import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; @@ -122,7 +121,6 @@ public class TakeScreenshotService extends Service { mContext = context; mBgExecutor = bgExecutor; mFeatureFlags = featureFlags; - mFeatureFlags.addListener(SCREENSHOT_REQUEST_PROCESSOR, FlagEvent::requestNoRestart); mFeatureFlags.addListener(SCREENSHOT_WORK_PROFILE_POLICY, FlagEvent::requestNoRestart); mProcessor = processor; } @@ -224,14 +222,8 @@ public class TakeScreenshotService extends Service { return; } - if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) { - Log.d(TAG, "handleMessage: Using request processor"); - mProcessor.processAsync(request, - (r) -> dispatchToController(r, onSaved, callback)); - return; - } - - dispatchToController(request, onSaved, callback); + mProcessor.processAsync(request, + (r) -> dispatchToController(r, onSaved, callback)); } private void dispatchToController(ScreenshotHelper.ScreenshotRequest request, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt new file mode 100644 index 000000000000..5d7e56f6c98a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -0,0 +1,106 @@ +/* + * 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.screenshot + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import com.android.systemui.R +import javax.inject.Inject + +/** + * Handles all the non-UI portions of the work profile first run: + * - Track whether the user has already dismissed it. + * - Load the proper icon and app name. + */ +class WorkProfileMessageController +@Inject +constructor( + private val context: Context, + private val userManager: UserManager, + private val packageManager: PackageManager, +) { + + /** + * Determine if a message should be shown to the user, send message details to messageDisplay if + * appropriate. + */ + fun onScreenshotTaken(userHandle: UserHandle, messageDisplay: WorkProfileMessageDisplay) { + if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) { + var badgedIcon: Drawable? = null + var label: CharSequence? = null + val fileManager = fileManagerComponentName() + try { + val info = + packageManager.getActivityInfo( + fileManager, + PackageManager.ComponentInfoFlags.of(0) + ) + val icon = packageManager.getActivityIcon(fileManager) + badgedIcon = packageManager.getUserBadgedIcon(icon, userHandle) + label = info.loadLabel(packageManager) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Component $fileManager not found") + } + + // If label wasn't loaded, use a default + val badgedLabel = + packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle) + + messageDisplay.showWorkProfileMessage(badgedLabel, badgedIcon) { onMessageDismissed() } + } + } + + private fun messageAlreadyDismissed(): Boolean { + return sharedPreference().getBoolean(PREFERENCE_KEY, false) + } + + private fun onMessageDismissed() { + val editor = sharedPreference().edit() + editor.putBoolean(PREFERENCE_KEY, true) + editor.apply() + } + + private fun sharedPreference() = + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + + private fun fileManagerComponentName() = + ComponentName.unflattenFromString( + context.getString(R.string.config_sceenshotWorkProfileFilesApp) + ) + + private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name) + + /** UI that can show work profile messages (ScreenshotView in practice) */ + interface WorkProfileMessageDisplay { + /** + * Show the given message and icon, calling onDismiss if the user explicitly dismisses the + * message. + */ + fun showWorkProfileMessage(text: CharSequence, icon: Drawable?, onDismiss: Runnable) + } + + companion object { + const val TAG = "WorkProfileMessageCtrl" + const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot" + const val PREFERENCE_KEY = "work_profile_first_run" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 28da38b701bc..61390c582fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -112,7 +112,7 @@ class UserTrackerImpl internal constructor( // These get called when a managed profile goes in or out of quiet mode. addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - + addAction(Intent.ACTION_MANAGED_PROFILE_ADDED) addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED) addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) } @@ -129,6 +129,7 @@ class UserTrackerImpl internal constructor( Intent.ACTION_USER_INFO_CHANGED, Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, Intent.ACTION_MANAGED_PROFILE_REMOVED, Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> { handleProfilesChanged() diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt index 5011227ad2cc..b3d31f2986d1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt @@ -69,7 +69,8 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain } return ConstraintsChanges( qqsConstraintsChanges = change, - qsConstraintsChanges = change + qsConstraintsChanges = change, + largeScreenConstraintsChanges = change, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index 7fc0a5f6d4bf..88676371bd6b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -113,7 +113,7 @@ class LargeScreenShadeHeaderController @Inject constructor( QQS_HEADER_CONSTRAINT -> "QQS Header" QS_HEADER_CONSTRAINT -> "QS Header" LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header" - else -> "Unknown state" + else -> "Unknown state $this" } } @@ -175,9 +175,10 @@ class LargeScreenShadeHeaderController @Inject constructor( */ var shadeExpandedFraction = -1f set(value) { - if (visible && field != value) { + if (field != value) { header.alpha = ShadeInterpolation.getContentAlpha(value) field = value + updateVisibility() } } @@ -295,6 +296,9 @@ class LargeScreenShadeHeaderController @Inject constructor( override fun onViewAttached() { privacyIconsController.chipVisibilityListener = chipVisibilityListener + updateVisibility() + updateTransition() + if (header is MotionLayout) { header.setOnApplyWindowInsetsListener(insetListener) clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> @@ -307,9 +311,6 @@ class LargeScreenShadeHeaderController @Inject constructor( dumpManager.registerDumpable(this) configurationController.addCallback(configurationControllerListener) demoModeController.addCallback(demoModeReceiver) - - updateVisibility() - updateTransition() } override fun onViewDetached() { @@ -331,6 +332,9 @@ class LargeScreenShadeHeaderController @Inject constructor( .setDuration(duration) .alpha(if (show) 0f else 1f) .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN) + .setUpdateListener { + updateVisibility() + } .start() } @@ -414,7 +418,7 @@ class LargeScreenShadeHeaderController @Inject constructor( private fun updateVisibility() { val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) { View.GONE - } else if (qsVisible) { + } else if (qsVisible && header.alpha > 0f) { View.VISIBLE } else { View.INVISIBLE @@ -432,15 +436,14 @@ class LargeScreenShadeHeaderController @Inject constructor( header as MotionLayout if (largeScreenActive) { logInstantEvent("Large screen constraints set") - header.setTransition(HEADER_TRANSITION_ID) - header.transitionToStart() + header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID) } else { logInstantEvent("Small screen constraints set") header.setTransition(HEADER_TRANSITION_ID) - header.transitionToStart() - updatePosition() - updateScrollY() } + header.jumpToState(header.startState) + updatePosition() + updateScrollY() } private fun updatePosition() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 8f512d0205b8..ce6fb14fbcf8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -138,12 +138,16 @@ import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; @@ -360,6 +364,7 @@ public final class NotificationPanelViewController implements Dumpable { private final FragmentListener mQsFragmentListener = new QsFragmentListener(); private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); private final NotificationGutsManager mGutsManager; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private long mDownTime; private boolean mTouchSlopExceededBeforeDown; @@ -445,6 +450,7 @@ public final class NotificationPanelViewController implements Dumpable { private float mDownY; private int mDisplayTopInset = 0; // in pixels private int mDisplayRightInset = 0; // in pixels + private int mDisplayLeftInset = 0; // in pixels private int mLargeScreenShadeHeaderHeight; private int mSplitShadeNotificationsScrimMarginBottom; @@ -687,12 +693,18 @@ public final class NotificationPanelViewController implements Dumpable { private boolean mExpandLatencyTracking; private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; + private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; + private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; + private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel; private KeyguardTransitionInteractor mKeyguardTransitionInteractor; private CoroutineDispatcher mMainDispatcher; - private boolean mIsToLockscreenTransitionRunning = false; + private boolean mIsOcclusionTransitionRunning = false; private int mDreamingToLockscreenTransitionTranslationY; private int mOccludedToLockscreenTransitionTranslationY; + private int mLockscreenToDreamingTransitionTranslationY; + private int mGoneToDreamingTransitionTranslationY; + private int mLockscreenToOccludedTransitionTranslationY; private boolean mUnocclusionTransitionFlagEnabled = false; private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, @@ -704,20 +716,38 @@ public final class NotificationPanelViewController implements Dumpable { updatePanelExpansionAndVisibility(); }; private final Runnable mMaybeHideExpandedRunnable = () -> { - if (getExpansionFraction() == 0.0f) { + if (getExpandedFraction() == 0.0f) { postToView(mHideExpandedRunnable); } }; private final Consumer<TransitionStep> mDreamingToLockscreenTransition = (TransitionStep step) -> { - mIsToLockscreenTransitionRunning = + mIsOcclusionTransitionRunning = step.getTransitionState() == TransitionState.RUNNING; }; private final Consumer<TransitionStep> mOccludedToLockscreenTransition = (TransitionStep step) -> { - mIsToLockscreenTransitionRunning = + mIsOcclusionTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + + private final Consumer<TransitionStep> mLockscreenToDreamingTransition = + (TransitionStep step) -> { + mIsOcclusionTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + + private final Consumer<TransitionStep> mGoneToDreamingTransition = + (TransitionStep step) -> { + mIsOcclusionTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + + private final Consumer<TransitionStep> mLockscreenToOccludedTransition = + (TransitionStep step) -> { + mIsOcclusionTransitionRunning = step.getTransitionState() == TransitionState.RUNNING; }; @@ -789,8 +819,12 @@ public final class NotificationPanelViewController implements Dumpable { SystemClock systemClock, KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, + AlternateBouncerInteractor alternateBouncerInteractor, DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel, OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel, + LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel, + GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel, + LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel, @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, DumpManager dumpManager) { @@ -810,6 +844,9 @@ public final class NotificationPanelViewController implements Dumpable { mGutsManager = gutsManager; mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel; + mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel; + mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel; + mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override @@ -980,6 +1017,7 @@ public final class NotificationPanelViewController implements Dumpable { unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay); } }); + mAlternateBouncerInteractor = alternateBouncerInteractor; dumpManager.registerDumpable(this); } @@ -1117,22 +1155,55 @@ public final class NotificationPanelViewController implements Dumpable { collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), mDreamingToLockscreenTransition, mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY( mDreamingToLockscreenTransitionTranslationY), - toLockscreenTransitionY(mNotificationStackScrollLayoutController), + setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); // Occluded->Lockscreen collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), mOccludedToLockscreenTransition, mMainDispatcher); collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), - toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY( mOccludedToLockscreenTransitionTranslationY), - toLockscreenTransitionY(mNotificationStackScrollLayoutController), + setTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); + + // Lockscreen->Dreaming + collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(), + mLockscreenToDreamingTransition, mMainDispatcher); + collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(), + setTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY( + mLockscreenToDreamingTransitionTranslationY), + setTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); + + // Gone->Dreaming + collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(), + mGoneToDreamingTransition, mMainDispatcher); + collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(), + setTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY( + mGoneToDreamingTransitionTranslationY), + setTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); + + // Lockscreen->Occluded + collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(), + mLockscreenToOccludedTransition, mMainDispatcher); + collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(), + setTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mLockscreenToOccludedTransitionViewModel.lockscreenTranslationY( + mLockscreenToOccludedTransitionTranslationY), + setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); } } @@ -1173,6 +1244,12 @@ public final class NotificationPanelViewController implements Dumpable { R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y); mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize( R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y); + mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y); + mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.gone_to_dreaming_transition_lockscreen_translation_y); + mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y); } private void updateViewControllers(KeyguardStatusView keyguardStatusView, @@ -1836,7 +1913,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateClock() { - if (mIsToLockscreenTransitionRunning) { + if (mIsOcclusionTransitionRunning) { return; } float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha; @@ -2288,7 +2365,7 @@ public final class NotificationPanelViewController implements Dumpable { // When false, down but not synthesized motion event. mLastEventSynthesizedDown = mExpectingSynthesizedDown; mLastDownEvents.insert( - mSystemClock.currentTimeMillis(), + event.getEventTime(), mDownX, mDownY, mQsTouchAboveFalsingThreshold, @@ -2421,7 +2498,7 @@ public final class NotificationPanelViewController implements Dumpable { mInitialTouchY = event.getY(); mInitialTouchX = event.getX(); } - if (!isFullyCollapsed()) { + if (!isFullyCollapsed() && !isShadeOrQsHeightAnimationRunning()) { handleQsDown(event); } // defer touches on QQS to shade while shade is collapsing. Added margin for error @@ -2727,7 +2804,7 @@ public final class NotificationPanelViewController implements Dumpable { } else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) { mKeyguardBottomArea.setVisibility(View.VISIBLE); - if (!mIsToLockscreenTransitionRunning) { + if (!mIsOcclusionTransitionRunning) { mKeyguardBottomArea.setAlpha(1f); } } else { @@ -2935,7 +3012,7 @@ public final class NotificationPanelViewController implements Dumpable { // left bounds can ignore insets, it should always reach the edge of the screen return 0; } else { - return mNotificationStackScrollLayoutController.getLeft(); + return mNotificationStackScrollLayoutController.getLeft() + mDisplayLeftInset; } } @@ -2943,7 +3020,7 @@ public final class NotificationPanelViewController implements Dumpable { if (mIsFullWidth) { return mView.getRight() + mDisplayRightInset; } else { - return mNotificationStackScrollLayoutController.getRight(); + return mNotificationStackScrollLayoutController.getRight() + mDisplayLeftInset; } } @@ -3067,8 +3144,8 @@ public final class NotificationPanelViewController implements Dumpable { // Convert global clipping coordinates to local ones, // relative to NotificationStackScrollLayout - int nsslLeft = left - mNotificationStackScrollLayoutController.getLeft(); - int nsslRight = right - mNotificationStackScrollLayoutController.getLeft(); + int nsslLeft = calculateNsslLeft(left); + int nsslRight = calculateNsslRight(right); int nsslTop = getNotificationsClippingTopBounds(top); int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop(); int bottomRadius = mSplitShadeEnabled ? radius : 0; @@ -3077,6 +3154,22 @@ public final class NotificationPanelViewController implements Dumpable { nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius); } + private int calculateNsslLeft(int nsslLeftAbsolute) { + int left = nsslLeftAbsolute - mNotificationStackScrollLayoutController.getLeft(); + if (mIsFullWidth) { + return left; + } + return left - mDisplayLeftInset; + } + + private int calculateNsslRight(int nsslRightAbsolute) { + int right = nsslRightAbsolute - mNotificationStackScrollLayoutController.getLeft(); + if (mIsFullWidth) { + return right; + } + return right - mDisplayLeftInset; + } + private int getNotificationsClippingTopBounds(int qsTop) { if (mSplitShadeEnabled && mExpandingFromHeadsUp) { // in split shade nssl has extra top margin so clipping at top 0 is not enough, we need @@ -3596,7 +3689,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateNotificationTranslucency() { - if (mIsToLockscreenTransitionRunning) { + if (mIsOcclusionTransitionRunning) { return; } float alpha = 1f; @@ -3654,7 +3747,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateKeyguardBottomAreaAlpha() { - if (mIsToLockscreenTransitionRunning) { + if (mIsOcclusionTransitionRunning) { return; } // There are two possible panel expansion behaviors: @@ -4474,6 +4567,7 @@ public final class NotificationPanelViewController implements Dumpable { ipw.print("mDownY="); ipw.println(mDownY); ipw.print("mDisplayTopInset="); ipw.println(mDisplayTopInset); ipw.print("mDisplayRightInset="); ipw.println(mDisplayRightInset); + ipw.print("mDisplayLeftInset="); ipw.println(mDisplayLeftInset); ipw.print("mLargeScreenShadeHeaderHeight="); ipw.println(mLargeScreenShadeHeaderHeight); ipw.print("mSplitShadeNotificationsScrimMarginBottom="); ipw.println(mSplitShadeNotificationsScrimMarginBottom); @@ -4902,7 +4996,7 @@ public final class NotificationPanelViewController implements Dumpable { mUpdateFlingVelocity = vel; } } else if (!mCentralSurfaces.isBouncerShowing() - && !mStatusBarKeyguardViewManager.isShowingAlternateBouncer() + && !mAlternateBouncerInteractor.isVisibleState() && !mKeyguardStateController.isKeyguardGoingAway()) { onEmptySpaceClick(); onTrackingStopped(true); @@ -5211,6 +5305,11 @@ public final class NotificationPanelViewController implements Dumpable { } } + /** Returns whether a shade or QS expansion animation is running */ + private boolean isShadeOrQsHeightAnimationRunning() { + return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation; + } + /** * Phase 2: Bounce down. */ @@ -5370,10 +5469,6 @@ public final class NotificationPanelViewController implements Dumpable { InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); } - private float getExpansionFraction() { - return mExpandedFraction; - } - private ShadeExpansionStateManager getShadeExpansionStateManager() { return mShadeExpansionStateManager; } @@ -5850,6 +5945,7 @@ public final class NotificationPanelViewController implements Dumpable { Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes); mDisplayTopInset = combinedInsets.top; mDisplayRightInset = combinedInsets.right; + mDisplayLeftInset = combinedInsets.left; mNavigationBarBottomHeight = insets.getStableInsetBottom(); updateMaxHeadsUpTranslation(); @@ -5886,7 +5982,7 @@ public final class NotificationPanelViewController implements Dumpable { mCurrentPanelState = state; } - private Consumer<Float> toLockscreenTransitionAlpha( + private Consumer<Float> setTransitionAlpha( NotificationStackScrollLayoutController stackScroller) { return (Float alpha) -> { mKeyguardStatusViewController.setAlpha(alpha); @@ -5904,7 +6000,7 @@ public final class NotificationPanelViewController implements Dumpable { }; } - private Consumer<Float> toLockscreenTransitionY( + private Consumer<Float> setTransitionY( NotificationStackScrollLayoutController stackScroller) { return (Float translationY) -> { mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false); @@ -6228,8 +6324,7 @@ public final class NotificationPanelViewController implements Dumpable { mCollapsedAndHeadsUpOnDown = isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); addMovement(event); - boolean regularHeightAnimationRunning = mHeightAnimator != null - && !mHintAnimationRunning && !mIsSpringBackAnimation; + boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning(); if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { mTouchSlopExceeded = regularHeightAnimationRunning || mTouchSlopExceededBeforeDown; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 8314ec713ccb..ab2e692915ad 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -19,6 +19,7 @@ package com.android.systemui.shade; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE; import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT; @@ -52,13 +53,13 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.DumpsysTableLogger; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -72,10 +73,8 @@ import java.io.PrintWriter; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -89,6 +88,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW Dumpable, ConfigurationListener { private static final String TAG = "NotificationShadeWindowController"; + private static final int MAX_STATE_CHANGES_BUFFER_SIZE = 100; private final Context mContext; private final WindowManager mWindowManager; @@ -108,7 +108,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private boolean mHasTopUi; private boolean mHasTopUiChanged; private float mScreenBrightnessDoze; - private final State mCurrentState = new State(); + private final NotificationShadeWindowState mCurrentState = new NotificationShadeWindowState(); private OtherwisedCollapsedListener mListener; private ForcePluginOpenListener mForcePluginOpenListener; private Consumer<Integer> mScrimsVisibilityListener; @@ -125,6 +125,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private int mDeferWindowLayoutParams; private boolean mLastKeyguardRotationAllowed; + private final NotificationShadeWindowState.Buffer mStateBuffer = + new NotificationShadeWindowState.Buffer(MAX_STATE_CHANGES_BUFFER_SIZE); + @Inject public NotificationShadeWindowControllerImpl(Context context, WindowManager windowManager, IActivityManager activityManager, DozeParameters dozeParameters, @@ -210,8 +213,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @VisibleForTesting void onShadeExpansionFullyChanged(Boolean isExpanded) { - if (mCurrentState.mPanelExpanded != isExpanded) { - mCurrentState.mPanelExpanded = isExpanded; + if (mCurrentState.panelExpanded != isExpanded) { + mCurrentState.panelExpanded = isExpanded; apply(mCurrentState); } } @@ -251,6 +254,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mLp.setTitle("NotificationShade"); mLp.packageName = mContext.getPackageName(); mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mLp.privateFlags |= PRIVATE_FLAG_OPTIMIZE_MEASURE; // We use BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE here, however, there is special logic in // window manager which disables the transient show behavior. @@ -296,10 +300,10 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mNotificationShadeView.setSystemUiVisibility(vis); } - private void applyKeyguardFlags(State state) { - final boolean keyguardOrAod = state.mKeyguardShowing - || (state.mDozing && mDozeParameters.getAlwaysOn()); - if ((keyguardOrAod && !state.mBackdropShowing && !state.mLightRevealScrimOpaque) + private void applyKeyguardFlags(NotificationShadeWindowState state) { + final boolean keyguardOrAod = state.keyguardShowing + || (state.dozing && mDozeParameters.getAlwaysOn()); + if ((keyguardOrAod && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque) || mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()) { // Show the wallpaper if we're on keyguard/AOD and the wallpaper is not occluded by a // solid backdrop. Also, show it if we are currently animating between the @@ -310,28 +314,31 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mLpChanged.flags &= ~LayoutParams.FLAG_SHOW_WALLPAPER; } - if (state.mDozing) { + if (state.dozing) { mLpChanged.privateFlags |= LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; } else { mLpChanged.privateFlags &= ~LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; } if (mKeyguardPreferredRefreshRate > 0) { - boolean onKeyguard = state.mStatusBarState == StatusBarState.KEYGUARD - && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway; + boolean onKeyguard = state.statusBarState == StatusBarState.KEYGUARD + && !state.keyguardFadingAway && !state.keyguardGoingAway; if (onKeyguard && mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) { + // both max and min display refresh rate must be set to take effect: mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate; + mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate; } else { mLpChanged.preferredMaxDisplayRefreshRate = 0; + mLpChanged.preferredMinDisplayRefreshRate = 0; } Trace.setCounter("display_set_preferred_refresh_rate", (long) mKeyguardPreferredRefreshRate); } else if (mKeyguardMaxRefreshRate > 0) { boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled() - && state.mStatusBarState == StatusBarState.KEYGUARD - && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway; - if (state.mDozing || bypassOnKeyguard) { + && state.statusBarState == StatusBarState.KEYGUARD + && !state.keyguardFadingAway && !state.keyguardGoingAway; + if (state.dozing || bypassOnKeyguard) { mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardMaxRefreshRate; } else { mLpChanged.preferredMaxDisplayRefreshRate = 0; @@ -340,7 +347,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW (long) mLpChanged.preferredMaxDisplayRefreshRate); } - if (state.mBouncerShowing && !isDebuggable()) { + if (state.bouncerShowing && !isDebuggable()) { mLpChanged.flags |= LayoutParams.FLAG_SECURE; } else { mLpChanged.flags &= ~LayoutParams.FLAG_SECURE; @@ -351,8 +358,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW return Build.IS_DEBUGGABLE; } - private void adjustScreenOrientation(State state) { - if (state.mBouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.mDozing) { + private void adjustScreenOrientation(NotificationShadeWindowState state) { + if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) { if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) { mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; } else { @@ -363,10 +370,10 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private void applyFocusableFlag(State state) { - boolean panelFocusable = state.mNotificationShadeFocusable && state.mPanelExpanded; - if (state.mBouncerShowing && (state.mKeyguardOccluded || state.mKeyguardNeedsInput) - || ENABLE_REMOTE_INPUT && state.mRemoteInputActive + private void applyFocusableFlag(NotificationShadeWindowState state) { + boolean panelFocusable = state.notificationShadeFocusable && state.panelExpanded; + if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput) + || ENABLE_REMOTE_INPUT && state.remoteInputActive // Make the panel focusable if we're doing the screen off animation, since the light // reveal scrim is drawing in the panel and should consume touch events so that they // don't go to the app behind. @@ -376,7 +383,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; // Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input. - if (state.mKeyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) { + if (state.keyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) { mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else { mLpChanged.flags |= LayoutParams.FLAG_ALT_FOCUSABLE_IM; @@ -387,19 +394,19 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private void applyForceShowNavigationFlag(State state) { - if (state.mPanelExpanded || state.mBouncerShowing - || ENABLE_REMOTE_INPUT && state.mRemoteInputActive) { + private void applyForceShowNavigationFlag(NotificationShadeWindowState state) { + if (state.panelExpanded || state.bouncerShowing + || ENABLE_REMOTE_INPUT && state.remoteInputActive) { mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; } else { mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; } } - private void applyVisibility(State state) { + private void applyVisibility(NotificationShadeWindowState state) { boolean visible = isExpanded(state); mLogger.logApplyVisibility(visible); - if (state.mForcePluginOpen) { + if (state.forcePluginOpen) { if (mListener != null) { mListener.setWouldOtherwiseCollapse(visible); } @@ -415,16 +422,16 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private boolean isExpanded(State state) { - return !state.mForceCollapsed && (state.isKeyguardShowingAndNotOccluded() - || state.mPanelVisible || state.mKeyguardFadingAway || state.mBouncerShowing - || state.mHeadsUpShowing - || state.mScrimsVisibility != ScrimController.TRANSPARENT) - || state.mBackgroundBlurRadius > 0 - || state.mLaunchingActivity; + private boolean isExpanded(NotificationShadeWindowState state) { + return !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded() + || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing + || state.headsUpNotificationShowing + || state.scrimsVisibility != ScrimController.TRANSPARENT) + || state.backgroundBlurRadius > 0 + || state.launchingActivityFromNotification; } - private void applyFitsSystemWindows(State state) { + private void applyFitsSystemWindows(NotificationShadeWindowState state) { boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded(); if (mNotificationShadeView != null && mNotificationShadeView.getFitsSystemWindows() != fitsSystemWindows) { @@ -433,21 +440,21 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private void applyUserActivityTimeout(State state) { + private void applyUserActivityTimeout(NotificationShadeWindowState state) { if (state.isKeyguardShowingAndNotOccluded() - && state.mStatusBarState == StatusBarState.KEYGUARD - && !state.mQsExpanded) { - mLpChanged.userActivityTimeout = state.mBouncerShowing + && state.statusBarState == StatusBarState.KEYGUARD + && !state.qsExpanded) { + mLpChanged.userActivityTimeout = state.bouncerShowing ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout; } else { mLpChanged.userActivityTimeout = -1; } } - private void applyInputFeatures(State state) { + private void applyInputFeatures(NotificationShadeWindowState state) { if (state.isKeyguardShowingAndNotOccluded() - && state.mStatusBarState == StatusBarState.KEYGUARD - && !state.mQsExpanded && !state.mForceUserActivity) { + && state.statusBarState == StatusBarState.KEYGUARD + && !state.qsExpanded && !state.forceUserActivity) { mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; } else { @@ -456,7 +463,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private void applyStatusBarColorSpaceAgnosticFlag(State state) { + private void applyStatusBarColorSpaceAgnosticFlag(NotificationShadeWindowState state) { if (!isExpanded(state)) { mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC; } else { @@ -482,8 +489,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW applyWindowLayoutParams(); } - private void apply(State state) { - mLogger.logNewState(state); + private void apply(NotificationShadeWindowState state) { + logState(state); applyKeyguardFlags(state); applyFocusableFlag(state); applyForceShowNavigationFlag(state); @@ -512,6 +519,38 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW notifyStateChangedCallbacks(); } + private void logState(NotificationShadeWindowState state) { + mStateBuffer.insert( + state.keyguardShowing, + state.keyguardOccluded, + state.keyguardNeedsInput, + state.panelVisible, + state.panelExpanded, + state.notificationShadeFocusable, + state.bouncerShowing, + state.keyguardFadingAway, + state.keyguardGoingAway, + state.qsExpanded, + state.headsUpNotificationShowing, + state.lightRevealScrimOpaque, + state.forceWindowCollapsed, + state.forceDozeBrightness, + state.forceUserActivity, + state.launchingActivityFromNotification, + state.mediaBackdropShowing, + state.wallpaperSupportsAmbientMode, + state.windowNotTouchable, + state.componentsForcingTopUi, + state.forceOpenTokens, + state.statusBarState, + state.remoteInputActive, + state.forcePluginOpen, + state.dozing, + state.scrimsVisibility, + state.backgroundBlurRadius + ); + } + @Override public void notifyStateChangedCallbacks() { // Copy callbacks to separate ArrayList to avoid concurrent modification @@ -520,36 +559,36 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW .filter(Objects::nonNull) .collect(Collectors.toList()); for (StatusBarWindowCallback cb : activeCallbacks) { - cb.onStateChanged(mCurrentState.mKeyguardShowing, - mCurrentState.mKeyguardOccluded, - mCurrentState.mBouncerShowing, - mCurrentState.mDozing, - mCurrentState.mPanelExpanded); + cb.onStateChanged(mCurrentState.keyguardShowing, + mCurrentState.keyguardOccluded, + mCurrentState.bouncerShowing, + mCurrentState.dozing, + mCurrentState.panelExpanded); } } - private void applyModalFlag(State state) { - if (state.mHeadsUpShowing) { + private void applyModalFlag(NotificationShadeWindowState state) { + if (state.headsUpNotificationShowing) { mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCH_MODAL; } else { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCH_MODAL; } } - private void applyBrightness(State state) { - if (state.mForceDozeBrightness) { + private void applyBrightness(NotificationShadeWindowState state) { + if (state.forceDozeBrightness) { mLpChanged.screenBrightness = mScreenBrightnessDoze; } else { mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; } } - private void applyHasTopUi(State state) { - mHasTopUiChanged = !state.mComponentsForcingTopUi.isEmpty() || isExpanded(state); + private void applyHasTopUi(NotificationShadeWindowState state) { + mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state); } - private void applyNotTouchable(State state) { - if (state.mNotTouchable) { + private void applyNotTouchable(NotificationShadeWindowState state) { + if (state.windowNotTouchable) { mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCHABLE; } else { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCHABLE; @@ -571,88 +610,88 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setKeyguardShowing(boolean showing) { - mCurrentState.mKeyguardShowing = showing; + mCurrentState.keyguardShowing = showing; apply(mCurrentState); } @Override public void setKeyguardOccluded(boolean occluded) { - mCurrentState.mKeyguardOccluded = occluded; + mCurrentState.keyguardOccluded = occluded; apply(mCurrentState); } @Override public void setKeyguardNeedsInput(boolean needsInput) { - mCurrentState.mKeyguardNeedsInput = needsInput; + mCurrentState.keyguardNeedsInput = needsInput; apply(mCurrentState); } @Override public void setPanelVisible(boolean visible) { - if (mCurrentState.mPanelVisible == visible - && mCurrentState.mNotificationShadeFocusable == visible) { + if (mCurrentState.panelVisible == visible + && mCurrentState.notificationShadeFocusable == visible) { return; } mLogger.logShadeVisibleAndFocusable(visible); - mCurrentState.mPanelVisible = visible; - mCurrentState.mNotificationShadeFocusable = visible; + mCurrentState.panelVisible = visible; + mCurrentState.notificationShadeFocusable = visible; apply(mCurrentState); } @Override public void setNotificationShadeFocusable(boolean focusable) { mLogger.logShadeFocusable(focusable); - mCurrentState.mNotificationShadeFocusable = focusable; + mCurrentState.notificationShadeFocusable = focusable; apply(mCurrentState); } @Override public void setBouncerShowing(boolean showing) { - mCurrentState.mBouncerShowing = showing; + mCurrentState.bouncerShowing = showing; apply(mCurrentState); } @Override public void setBackdropShowing(boolean showing) { - mCurrentState.mBackdropShowing = showing; + mCurrentState.mediaBackdropShowing = showing; apply(mCurrentState); } @Override public void setKeyguardFadingAway(boolean keyguardFadingAway) { - mCurrentState.mKeyguardFadingAway = keyguardFadingAway; + mCurrentState.keyguardFadingAway = keyguardFadingAway; apply(mCurrentState); } private void onQsExpansionChanged(Boolean expanded) { - mCurrentState.mQsExpanded = expanded; + mCurrentState.qsExpanded = expanded; apply(mCurrentState); } @Override public void setForceUserActivity(boolean forceUserActivity) { - mCurrentState.mForceUserActivity = forceUserActivity; + mCurrentState.forceUserActivity = forceUserActivity; apply(mCurrentState); } @Override public void setLaunchingActivity(boolean launching) { - mCurrentState.mLaunchingActivity = launching; + mCurrentState.launchingActivityFromNotification = launching; apply(mCurrentState); } @Override public boolean isLaunchingActivity() { - return mCurrentState.mLaunchingActivity; + return mCurrentState.launchingActivityFromNotification; } @Override public void setScrimsVisibility(int scrimsVisibility) { - if (scrimsVisibility == mCurrentState.mScrimsVisibility) { + if (scrimsVisibility == mCurrentState.scrimsVisibility) { return; } boolean wasExpanded = isExpanded(mCurrentState); - mCurrentState.mScrimsVisibility = scrimsVisibility; + mCurrentState.scrimsVisibility = scrimsVisibility; if (wasExpanded != isExpanded(mCurrentState)) { apply(mCurrentState); } @@ -666,31 +705,31 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setBackgroundBlurRadius(int backgroundBlurRadius) { - if (mCurrentState.mBackgroundBlurRadius == backgroundBlurRadius) { + if (mCurrentState.backgroundBlurRadius == backgroundBlurRadius) { return; } - mCurrentState.mBackgroundBlurRadius = backgroundBlurRadius; + mCurrentState.backgroundBlurRadius = backgroundBlurRadius; apply(mCurrentState); } @Override public void setHeadsUpShowing(boolean showing) { - mCurrentState.mHeadsUpShowing = showing; + mCurrentState.headsUpNotificationShowing = showing; apply(mCurrentState); } @Override public void setLightRevealScrimOpaque(boolean opaque) { - if (mCurrentState.mLightRevealScrimOpaque == opaque) { + if (mCurrentState.lightRevealScrimOpaque == opaque) { return; } - mCurrentState.mLightRevealScrimOpaque = opaque; + mCurrentState.lightRevealScrimOpaque = opaque; apply(mCurrentState); } @Override public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) { - mCurrentState.mWallpaperSupportsAmbientMode = supportsAmbientMode; + mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode; apply(mCurrentState); } @@ -698,7 +737,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW * @param state The {@link StatusBarStateController} of the status bar. */ private void setStatusBarState(int state) { - mCurrentState.mStatusBarState = state; + mCurrentState.statusBarState = state; apply(mCurrentState); } @@ -709,13 +748,13 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setForceWindowCollapsed(boolean force) { - mCurrentState.mForceCollapsed = force; + mCurrentState.forceWindowCollapsed = force; apply(mCurrentState); } @Override public void onRemoteInputActive(boolean remoteInputActive) { - mCurrentState.mRemoteInputActive = remoteInputActive; + mCurrentState.remoteInputActive = remoteInputActive; apply(mCurrentState); } @@ -725,32 +764,32 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setForceDozeBrightness(boolean forceDozeBrightness) { - if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) { + if (mCurrentState.forceDozeBrightness == forceDozeBrightness) { return; } - mCurrentState.mForceDozeBrightness = forceDozeBrightness; + mCurrentState.forceDozeBrightness = forceDozeBrightness; apply(mCurrentState); } @Override public void setDozing(boolean dozing) { - mCurrentState.mDozing = dozing; + mCurrentState.dozing = dozing; apply(mCurrentState); } @Override public void setForcePluginOpen(boolean forceOpen, Object token) { if (forceOpen) { - mCurrentState.mForceOpenTokens.add(token); + mCurrentState.forceOpenTokens.add(token); } else { - mCurrentState.mForceOpenTokens.remove(token); + mCurrentState.forceOpenTokens.remove(token); } - final boolean previousForceOpenState = mCurrentState.mForcePluginOpen; - mCurrentState.mForcePluginOpen = !mCurrentState.mForceOpenTokens.isEmpty(); - if (previousForceOpenState != mCurrentState.mForcePluginOpen) { + final boolean previousForceOpenState = mCurrentState.forcePluginOpen; + mCurrentState.forcePluginOpen = !mCurrentState.forceOpenTokens.isEmpty(); + if (previousForceOpenState != mCurrentState.forcePluginOpen) { apply(mCurrentState); if (mForcePluginOpenListener != null) { - mForcePluginOpenListener.onChange(mCurrentState.mForcePluginOpen); + mForcePluginOpenListener.onChange(mCurrentState.forcePluginOpen); } } } @@ -760,12 +799,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public boolean getForcePluginOpen() { - return mCurrentState.mForcePluginOpen; + return mCurrentState.forcePluginOpen; } @Override public void setNotTouchable(boolean notTouchable) { - mCurrentState.mNotTouchable = notTouchable; + mCurrentState.windowNotTouchable = notTouchable; apply(mCurrentState); } @@ -774,7 +813,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public boolean getPanelExpanded() { - return mCurrentState.mPanelExpanded; + return mCurrentState.panelExpanded; } @Override @@ -797,11 +836,16 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) { mNotificationShadeView.getViewRootImpl().dump(" ", pw); } + new DumpsysTableLogger( + TAG, + NotificationShadeWindowState.TABLE_HEADERS, + mStateBuffer.toList() + ).printTableData(pw); } @Override public boolean isShowingWallpaper() { - return !mCurrentState.mBackdropShowing; + return !mCurrentState.mediaBackdropShowing; } @Override @@ -831,7 +875,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setKeyguardGoingAway(boolean goingAway) { - mCurrentState.mKeyguardGoingAway = goingAway; + mCurrentState.keyguardGoingAway = goingAway; apply(mCurrentState); } @@ -843,87 +887,13 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setRequestTopUi(boolean requestTopUi, String componentTag) { if (requestTopUi) { - mCurrentState.mComponentsForcingTopUi.add(componentTag); + mCurrentState.componentsForcingTopUi.add(componentTag); } else { - mCurrentState.mComponentsForcingTopUi.remove(componentTag); + mCurrentState.componentsForcingTopUi.remove(componentTag); } apply(mCurrentState); } - private static class State { - boolean mKeyguardShowing; - boolean mKeyguardOccluded; - boolean mKeyguardNeedsInput; - boolean mPanelVisible; - boolean mPanelExpanded; - boolean mNotificationShadeFocusable; - boolean mBouncerShowing; - boolean mKeyguardFadingAway; - boolean mKeyguardGoingAway; - boolean mQsExpanded; - boolean mHeadsUpShowing; - boolean mLightRevealScrimOpaque; - boolean mForceCollapsed; - boolean mForceDozeBrightness; - boolean mForceUserActivity; - boolean mLaunchingActivity; - boolean mBackdropShowing; - boolean mWallpaperSupportsAmbientMode; - boolean mNotTouchable; - Set<String> mComponentsForcingTopUi = new HashSet<>(); - Set<Object> mForceOpenTokens = new HashSet<>(); - - /** - * The status bar state from {@link CentralSurfaces}. - */ - int mStatusBarState; - - boolean mRemoteInputActive; - boolean mForcePluginOpen; - boolean mDozing; - int mScrimsVisibility; - int mBackgroundBlurRadius; - - private boolean isKeyguardShowingAndNotOccluded() { - return mKeyguardShowing && !mKeyguardOccluded; - } - - @Override - public String toString() { - return new StringBuilder() - .append("State{") - .append(" mKeyguardShowing=").append(mKeyguardShowing) - .append(", mKeyguardOccluded=").append(mKeyguardOccluded) - .append(", mKeyguardNeedsInput=").append(mKeyguardNeedsInput) - .append(", mPanelVisible=").append(mPanelVisible) - .append(", mPanelExpanded=").append(mPanelExpanded) - .append(", mNotificationShadeFocusable=").append(mNotificationShadeFocusable) - .append(", mBouncerShowing=").append(mBouncerShowing) - .append(", mKeyguardFadingAway=").append(mKeyguardFadingAway) - .append(", mKeyguardGoingAway=").append(mKeyguardGoingAway) - .append(", mQsExpanded=").append(mQsExpanded) - .append(", mHeadsUpShowing=").append(mHeadsUpShowing) - .append(", mLightRevealScrimOpaque=").append(mLightRevealScrimOpaque) - .append(", mForceCollapsed=").append(mForceCollapsed) - .append(", mForceDozeBrightness=").append(mForceDozeBrightness) - .append(", mForceUserActivity=").append(mForceUserActivity) - .append(", mLaunchingActivity=").append(mLaunchingActivity) - .append(", mBackdropShowing=").append(mBackdropShowing) - .append(", mWallpaperSupportsAmbientMode=") - .append(mWallpaperSupportsAmbientMode) - .append(", mNotTouchable=").append(mNotTouchable) - .append(", mComponentsForcingTopUi=").append(mComponentsForcingTopUi) - .append(", mForceOpenTokens=").append(mForceOpenTokens) - .append(", mStatusBarState=").append(mStatusBarState) - .append(", mRemoteInputActive=").append(mRemoteInputActive) - .append(", mForcePluginOpen=").append(mForcePluginOpen) - .append(", mDozing=").append(mDozing) - .append(", mScrimsVisibility=").append(mScrimsVisibility) - .append(", mBackgroundBlurRadius=").append(mBackgroundBlurRadius) - .append('}').toString(); - } - } - private final StateListener mStateListener = new StateListener() { @Override public void onStateChanged(int newState) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt new file mode 100644 index 000000000000..736404aa548a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -0,0 +1,214 @@ +/* + * 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 + +import com.android.systemui.dump.DumpsysTableLogger +import com.android.systemui.dump.Row +import com.android.systemui.plugins.util.RingBuffer +import com.android.systemui.shade.NotificationShadeWindowState.Buffer +import com.android.systemui.statusbar.StatusBarState + +/** + * Represents state of shade window, used by [NotificationShadeWindowControllerImpl]. + * Contains nested class [Buffer] for pretty table logging in bug reports. + */ +class NotificationShadeWindowState( + @JvmField var keyguardShowing: Boolean = false, + @JvmField var keyguardOccluded: Boolean = false, + @JvmField var keyguardNeedsInput: Boolean = false, + @JvmField var panelVisible: Boolean = false, + /** shade panel is expanded (expansion fraction > 0) */ + @JvmField var panelExpanded: Boolean = false, + @JvmField var notificationShadeFocusable: Boolean = false, + @JvmField var bouncerShowing: Boolean = false, + @JvmField var keyguardFadingAway: Boolean = false, + @JvmField var keyguardGoingAway: Boolean = false, + @JvmField var qsExpanded: Boolean = false, + @JvmField var headsUpNotificationShowing: Boolean = false, + @JvmField var lightRevealScrimOpaque: Boolean = false, + @JvmField var forceWindowCollapsed: Boolean = false, + @JvmField var forceDozeBrightness: Boolean = false, + // TODO: forceUserActivity seems to be unused, delete? + @JvmField var forceUserActivity: Boolean = false, + @JvmField var launchingActivityFromNotification: Boolean = false, + @JvmField var mediaBackdropShowing: Boolean = false, + @JvmField var wallpaperSupportsAmbientMode: Boolean = false, + @JvmField var windowNotTouchable: Boolean = false, + @JvmField var componentsForcingTopUi: MutableSet<String> = mutableSetOf(), + @JvmField var forceOpenTokens: MutableSet<Any> = mutableSetOf(), + /** one of [StatusBarState] */ + @JvmField var statusBarState: Int = 0, + @JvmField var remoteInputActive: Boolean = false, + @JvmField var forcePluginOpen: Boolean = false, + @JvmField var dozing: Boolean = false, + @JvmField var scrimsVisibility: Int = 0, + @JvmField var backgroundBlurRadius: Int = 0, +) { + + fun isKeyguardShowingAndNotOccluded(): Boolean { + return keyguardShowing && !keyguardOccluded + } + + /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ + val asStringList: List<String> by lazy { + listOf( + keyguardShowing.toString(), + keyguardOccluded.toString(), + keyguardNeedsInput.toString(), + panelVisible.toString(), + panelExpanded.toString(), + notificationShadeFocusable.toString(), + bouncerShowing.toString(), + keyguardFadingAway.toString(), + keyguardGoingAway.toString(), + qsExpanded.toString(), + headsUpNotificationShowing.toString(), + lightRevealScrimOpaque.toString(), + forceWindowCollapsed.toString(), + forceDozeBrightness.toString(), + forceUserActivity.toString(), + launchingActivityFromNotification.toString(), + mediaBackdropShowing.toString(), + wallpaperSupportsAmbientMode.toString(), + windowNotTouchable.toString(), + componentsForcingTopUi.toString(), + forceOpenTokens.toString(), + StatusBarState.toString(statusBarState), + remoteInputActive.toString(), + forcePluginOpen.toString(), + dozing.toString(), + scrimsVisibility.toString(), + backgroundBlurRadius.toString() + ) + } + + /** + * [RingBuffer] to store [NotificationShadeWindowState]. After the buffer is full, it will + * recycle old events. + */ + class Buffer(capacity: Int) { + + private val buffer = RingBuffer(capacity) { NotificationShadeWindowState() } + + /** Insert a new element in the buffer. */ + fun insert( + keyguardShowing: Boolean, + keyguardOccluded: Boolean, + keyguardNeedsInput: Boolean, + panelVisible: Boolean, + panelExpanded: Boolean, + notificationShadeFocusable: Boolean, + bouncerShowing: Boolean, + keyguardFadingAway: Boolean, + keyguardGoingAway: Boolean, + qsExpanded: Boolean, + headsUpShowing: Boolean, + lightRevealScrimOpaque: Boolean, + forceCollapsed: Boolean, + forceDozeBrightness: Boolean, + forceUserActivity: Boolean, + launchingActivity: Boolean, + backdropShowing: Boolean, + wallpaperSupportsAmbientMode: Boolean, + notTouchable: Boolean, + componentsForcingTopUi: MutableSet<String>, + forceOpenTokens: MutableSet<Any>, + statusBarState: Int, + remoteInputActive: Boolean, + forcePluginOpen: Boolean, + dozing: Boolean, + scrimsVisibility: Int, + backgroundBlurRadius: Int, + ) { + buffer.advance().apply { + this.keyguardShowing = keyguardShowing + this.keyguardOccluded = keyguardOccluded + this.keyguardNeedsInput = keyguardNeedsInput + this.panelVisible = panelVisible + this.panelExpanded = panelExpanded + this.notificationShadeFocusable = notificationShadeFocusable + this.bouncerShowing = bouncerShowing + this.keyguardFadingAway = keyguardFadingAway + this.keyguardGoingAway = keyguardGoingAway + this.qsExpanded = qsExpanded + this.headsUpNotificationShowing = headsUpShowing + this.lightRevealScrimOpaque = lightRevealScrimOpaque + this.forceWindowCollapsed = forceCollapsed + this.forceDozeBrightness = forceDozeBrightness + this.forceUserActivity = forceUserActivity + this.launchingActivityFromNotification = launchingActivity + this.mediaBackdropShowing = backdropShowing + this.wallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode + this.windowNotTouchable = notTouchable + this.componentsForcingTopUi.clear() + this.componentsForcingTopUi.addAll(componentsForcingTopUi) + this.forceOpenTokens.clear() + this.forceOpenTokens.addAll(forceOpenTokens) + this.statusBarState = statusBarState + this.remoteInputActive = remoteInputActive + this.forcePluginOpen = forcePluginOpen + this.dozing = dozing + this.scrimsVisibility = scrimsVisibility + this.backgroundBlurRadius = backgroundBlurRadius + } + } + + /** + * Returns the content of the buffer (sorted from latest to newest). + * + * @see [NotificationShadeWindowState.asStringList] + */ + fun toList(): List<Row> { + return buffer.asSequence().map { it.asStringList }.toList() + } + } + + companion object { + /** Headers for dumping a table using [DumpsysTableLogger]. */ + @JvmField + val TABLE_HEADERS = + listOf( + "keyguardShowing", + "keyguardOccluded", + "keyguardNeedsInput", + "panelVisible", + "panelExpanded", + "notificationShadeFocusable", + "bouncerShowing", + "keyguardFadingAway", + "keyguardGoingAway", + "qsExpanded", + "headsUpShowing", + "lightRevealScrimOpaque", + "forceCollapsed", + "forceDozeBrightness", + "forceUserActivity", + "launchingActivity", + "backdropShowing", + "wallpaperSupportsAmbientMode", + "notTouchable", + "componentsForcingTopUi", + "forceOpenTokens", + "statusBarState", + "remoteInputActive", + "forcePluginOpen", + "dozing", + "scrimsVisibility", + "backgroundBlurRadius" + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index 6acf417f0ea6..1f0cbf9af51c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -58,6 +58,7 @@ import android.widget.FrameLayout; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; import com.android.systemui.R; +import com.android.systemui.compose.ComposeFacade; /** * Combined keyguard and notification panel view. Also holding backdrop and scrims. @@ -149,6 +150,18 @@ public class NotificationShadeWindowView extends FrameLayout { protected void onAttachedToWindow() { super.onAttachedToWindow(); setWillNotDraw(!DEBUG); + + if (ComposeFacade.INSTANCE.isComposeAvailable()) { + ComposeFacade.INSTANCE.composeInitializer().onAttachedToWindow(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (ComposeFacade.INSTANCE.isComposeAvailable()) { + ComposeFacade.INSTANCE.composeInitializer().onDetachedFromWindow(this); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 5c1ddd601f96..7ed6e3e55623 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -16,6 +16,8 @@ package com.android.systemui.shade; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; + import android.app.StatusBarManager; import android.media.AudioManager; import android.media.session.MediaSessionLegacyHelper; @@ -39,6 +41,10 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.statusbar.DragDownHelper; @@ -57,6 +63,7 @@ import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import java.io.PrintWriter; +import java.util.function.Consumer; import javax.inject.Inject; @@ -79,6 +86,7 @@ public class NotificationShadeWindowViewController { private final AmbientState mAmbientState; private final PulsingGestureListener mPulsingGestureListener; private final NotificationInsetsController mNotificationInsetsController; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private GestureDetector mPulsingWakeupGestureHandler; private View mBrightnessMirror; @@ -96,6 +104,13 @@ public class NotificationShadeWindowViewController { private final ShadeExpansionStateManager mShadeExpansionStateManager; private boolean mIsTrackingBarGesture = false; + private boolean mIsOcclusionTransitionRunning = false; + + private final Consumer<TransitionStep> mLockscreenToDreamingTransition = + (TransitionStep step) -> { + mIsOcclusionTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; @Inject public NotificationShadeWindowViewController( @@ -119,7 +134,9 @@ public class NotificationShadeWindowViewController { PulsingGestureListener pulsingGestureListener, FeatureFlags featureFlags, KeyguardBouncerViewModel keyguardBouncerViewModel, - KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory + KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory, + AlternateBouncerInteractor alternateBouncerInteractor, + KeyguardTransitionInteractor keyguardTransitionInteractor ) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; @@ -139,6 +156,7 @@ public class NotificationShadeWindowViewController { mAmbientState = ambientState; mPulsingGestureListener = pulsingGestureListener; mNotificationInsetsController = notificationInsetsController; + mAlternateBouncerInteractor = alternateBouncerInteractor; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -148,6 +166,11 @@ public class NotificationShadeWindowViewController { keyguardBouncerViewModel, keyguardBouncerComponentFactory); } + + if (featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION)) { + collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), + mLockscreenToDreamingTransition); + } } /** @@ -215,6 +238,10 @@ public class NotificationShadeWindowViewController { return true; } + if (mIsOcclusionTransitionRunning) { + return false; + } + mFalsingCollector.onTouchEvent(ev); mPulsingWakeupGestureHandler.onTouchEvent(ev); mStatusBarKeyguardViewManager.onTouch(ev); @@ -292,7 +319,7 @@ public class NotificationShadeWindowViewController { return true; } - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { // capture all touches if the alt auth bouncer is showing return true; } @@ -330,7 +357,7 @@ public class NotificationShadeWindowViewController { handled = !mService.isPulsing(); } - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { // eat the touch handled = true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 5fedbeb556c2..11617be40f53 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -36,16 +36,9 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { buffer.log(TAG, LogLevel.DEBUG, msg) } - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } - fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { double1 = h.toDouble() }, { "onQsIntercept: move action, QS tracking enabled. h = $double1" } @@ -62,7 +55,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { keyguardShowing: Boolean, qsExpansionEnabled: Boolean ) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { int1 = initialTouchY.toInt() @@ -82,7 +76,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEvent(event: MotionEvent, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -99,7 +94,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEventStatusBarState(event: MotionEvent, statusBarState: Int, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -128,25 +124,33 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { tracking: Boolean, dragDownPxAmount: Float, ) { - log(LogLevel.VERBOSE, { - str1 = message - double1 = fraction.toDouble() - bool1 = expanded - bool2 = tracking - long1 = dragDownPxAmount.toLong() - }, { - "$str1 fraction=$double1,expanded=$bool1," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + double1 = fraction.toDouble() + bool1 = expanded + bool2 = tracking + long1 = dragDownPxAmount.toLong() + }, + { + "$str1 fraction=$double1,expanded=$bool1," + "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount" - }) + } + ) } fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) { - log(LogLevel.VERBOSE, { - bool1 = hasVibratedOnOpen - double1 = fraction.toDouble() - }, { - "hasVibratedOnOpen=$bool1, expansionFraction=$double1" - }) + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = hasVibratedOnOpen + double1 = fraction.toDouble() + }, + { "hasVibratedOnOpen=$bool1, expansionFraction=$double1" } + ) } fun logQsExpansionChanged( @@ -159,42 +163,56 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { qsAnimatorExpand: Boolean, animatingQs: Boolean ) { - log(LogLevel.VERBOSE, { - str1 = message - bool1 = qsExpanded - int1 = qsMinExpansionHeight - int2 = qsMaxExpansionHeight - bool2 = stackScrollerOverscrolling - bool3 = dozing - bool4 = qsAnimatorExpand - // 0 = false, 1 = true - long1 = animatingQs.compareTo(false).toLong() - }, { - "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + bool1 = qsExpanded + int1 = qsMinExpansionHeight + int2 = qsMaxExpansionHeight + bool2 = stackScrollerOverscrolling + bool3 = dozing + bool4 = qsAnimatorExpand + // 0 = false, 1 = true + long1 = animatingQs.compareTo(false).toLong() + }, + { + "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," + "animatingQs=$long1" - }) + } + ) } fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) { - log(LogLevel.DEBUG, { - bool1 = isDozing - bool2 = singleTapEnabled - bool3 = isNotDocked - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + - "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = isDozing + bool2 = singleTapEnabled + bool3 = isNotDocked + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + + "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" }) } fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) { - log(LogLevel.DEBUG, { - bool1 = proximityIsNotNear - bool2 = isNotFalseTap - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = proximityIsNotNear + bool2 = isNotFalseTap + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2" - }) + } + ) } fun logNotInterceptingTouchInstantExpanding( @@ -202,13 +220,18 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { notificationsDragEnabled: Boolean, touchDisabled: Boolean ) { - log(LogLevel.VERBOSE, { - bool1 = instantExpanding - bool2 = notificationsDragEnabled - bool3 = touchDisabled - }, { - "NPVC not intercepting touch, instantExpanding: $bool1, " + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = instantExpanding + bool2 = notificationsDragEnabled + bool3 = touchDisabled + }, + { + "NPVC not intercepting touch, instantExpanding: $bool1, " + "!notificationsDragEnabled: $bool2, touchDisabled: $bool3" - }) + } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt index c6a6e875b82d..9851625b6152 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt @@ -32,11 +32,21 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { fun logApplyingWindowLayoutParams(lp: WindowManager.LayoutParams) { - log(DEBUG, { str1 = lp.toString() }, { "Applying new window layout params: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = lp.toString() }, + { "Applying new window layout params: $str1" } + ) } fun logNewState(state: Any) { - log(DEBUG, { str1 = state.toString() }, { "Applying new state: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = state.toString() }, + { "Applying new state: $str1" } + ) } private inline fun log( @@ -48,11 +58,16 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logApplyVisibility(visible: Boolean) { - log(DEBUG, { bool1 = visible }, { "Updating visibility, should be visible : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = visible }, + { "Updating visibility, should be visible : $bool1" }) } fun logShadeVisibleAndFocusable(visible: Boolean) { - log( + buffer.log( + TAG, DEBUG, { bool1 = visible }, { "Updating shade, should be visible and focusable: $bool1" } @@ -60,6 +75,11 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logShadeFocusable(focusable: Boolean) { - log(DEBUG, { bool1 = focusable }, { "Updating shade, should be focusable : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = focusable }, + { "Updating shade, should be focusable : $bool1" } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt new file mode 100644 index 000000000000..ab0d6e3a6382 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartspace.config + +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.BcSmartspaceConfigPlugin + +class BcSmartspaceConfigProvider(private val featureFlags: FeatureFlags) : + BcSmartspaceConfigPlugin { + override val isDefaultDateWeatherDisabled: Boolean + get() = featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java index 662f70ef269e..438b0f625fc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java @@ -36,10 +36,6 @@ public class AlphaOptimizedFrameLayout extends FrameLayout implements Launchable visibility -> { super.setVisibility(visibility); return Unit.INSTANCE; - }, - visibility -> { - super.setTransitionVisibility(visibility); - return Unit.INSTANCE; }); public AlphaOptimizedFrameLayout(Context context) { @@ -73,9 +69,4 @@ public class AlphaOptimizedFrameLayout extends FrameLayout implements Launchable public void setVisibility(int visibility) { mLaunchableViewDelegate.setVisibility(visibility); } - - @Override - public void setTransitionVisibility(int visibility) { - mLaunchableViewDelegate.setTransitionVisibility(visibility); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 750d00466a8d..4e9f69ce7d35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -23,8 +23,6 @@ import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; -import static com.android.systemui.statusbar.phone.CentralSurfacesImpl.ONLY_CORE_APPS; - import android.annotation.Nullable; import android.app.ITransientNotificationCallback; import android.app.StatusBarManager; @@ -45,14 +43,17 @@ import android.hardware.fingerprint.IUdfpsHbmListener; import android.inputmethodservice.InputMethodService.BackDispositionMode; import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.RemoteException; import android.util.Pair; +import android.util.Slog; import android.util.SparseArray; import android.view.InsetsState.InternalInsetsType; import android.view.InsetsVisibilities; @@ -168,6 +169,7 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT; private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT; private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT; + private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -334,7 +336,7 @@ public class CommandQueue extends IStatusBar.Stub implements /** * @see IStatusBar#setBiometicContextListener(IBiometricContextListener) */ - default void setBiometicContextListener(IBiometricContextListener listener) { + default void setBiometricContextListener(IBiometricContextListener listener) { } /** @@ -492,6 +494,11 @@ public class CommandQueue extends IStatusBar.Stub implements * @see IStatusBar#enterStageSplitFromRunningApp */ default void enterStageSplitFromRunningApp(boolean leftOrTop) {} + + /** + * @see IStatusBar#showMediaOutputSwitcher + */ + default void showMediaOutputSwitcher(String packageName) {} } public CommandQueue(Context context) { @@ -535,8 +542,7 @@ public class CommandQueue extends IStatusBar.Stub implements final int disabled1 = getDisabled1(DEFAULT_DISPLAY); final int disabled2 = getDisabled2(DEFAULT_DISPLAY); return (disabled1 & StatusBarManager.DISABLE_EXPAND) == 0 - && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0 - && !ONLY_CORE_APPS; + && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0; } @Override @@ -1262,6 +1268,20 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void showMediaOutputSwitcher(String packageName) { + int callingUid = Binder.getCallingUid(); + if (callingUid != 0 && callingUid != Process.SYSTEM_UID) { + Slog.e(TAG, "Call only allowed from system server."); + return; + } + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = packageName; + mHandler.obtainMessage(MSG_SHOW_MEDIA_OUTPUT_SWITCHER, args).sendToTarget(); + } + } + + @Override public void requestAddTile( @NonNull ComponentName componentName, @NonNull CharSequence appName, @@ -1583,7 +1603,7 @@ public class CommandQueue extends IStatusBar.Stub implements } case MSG_SET_BIOMETRICS_LISTENER: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).setBiometicContextListener( + mCallbacks.get(i).setBiometricContextListener( (IBiometricContextListener) msg.obj); } break; @@ -1777,6 +1797,13 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj); } break; + case MSG_SHOW_MEDIA_OUTPUT_SWITCHER: + args = (SomeArgs) msg.obj; + String clientPackageName = (String) args.arg1; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName); + } + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java new file mode 100644 index 000000000000..a797d4a3a7b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar; + +import android.content.BroadcastReceiver; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +/** + * Module for {@link com.android.systemui.KeyboardShortcutsReceiver}. + */ +@Module +public abstract class KeyboardShortcutsModule { + + /** + * + */ + @Binds + @IntoMap + @ClassKey(KeyboardShortcutsReceiver.class) + public abstract BroadcastReceiver bindKeyboardShortcutsReceiver( + KeyboardShortcutsReceiver broadcastReceiver); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 6a658b6ee047..006b5528e7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -41,6 +41,7 @@ import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewCont import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; +import static com.android.systemui.plugins.log.LogLevel.ERROR; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -90,6 +91,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -155,6 +157,7 @@ public class KeyguardIndicationController { private final KeyguardBypassController mKeyguardBypassController; private final AccessibilityManager mAccessibilityManager; private final Handler mHandler; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; @VisibleForTesting public KeyguardIndicationRotateTextViewController mRotateTextViewController; @@ -234,7 +237,8 @@ public class KeyguardIndicationController { KeyguardBypassController keyguardBypassController, AccessibilityManager accessibilityManager, FaceHelpMessageDeferral faceHelpMessageDeferral, - KeyguardLogger keyguardLogger) { + KeyguardLogger keyguardLogger, + AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mDevicePolicyManager = devicePolicyManager; @@ -256,6 +260,7 @@ public class KeyguardIndicationController { mScreenLifecycle = screenLifecycle; mKeyguardLogger = keyguardLogger; mScreenLifecycle.addObserver(mScreenObserver); + mAlternateBouncerInteractor = alternateBouncerInteractor; mFaceAcquiredMessageDeferral = faceHelpMessageDeferral; mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>(); @@ -928,7 +933,7 @@ public class KeyguardIndicationController { } if (mStatusBarKeyguardViewManager.isBouncerShowing()) { - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { return; // udfps affordance is highlighted, no need to show action to unlock } else if (mKeyguardUpdateMonitor.isFaceEnrolled() && !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) { @@ -1028,7 +1033,7 @@ public class KeyguardIndicationController { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; } catch (RemoteException e) { - mKeyguardLogger.logException(e, "Error calling IBatteryStats"); + mKeyguardLogger.log(TAG, ERROR, "Error calling IBatteryStats", e); mChargingTimeRemaining = -1; } updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 905cc3fc71e0..f565f3daf2ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -509,9 +509,14 @@ class LockscreenShadeTransitionController @Inject constructor( * If secure with redaction: Show bouncer, go to unlocked shade. * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED]. * + * Split shade is special case and [needsQSAnimation] will be always overridden to true. + * That's because handheld shade will automatically follow notifications animation, but that's + * not the case for split shade. + * * @param expandView The view to expand after going to the shade * @param needsQSAnimation if this needs the quick settings to slide in from the top or if - * that's already handled separately + * that's already handled separately. This argument will be ignored on + * split shade as there QS animation can't be handled separately. */ @JvmOverloads fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) { @@ -519,7 +524,7 @@ class LockscreenShadeTransitionController @Inject constructor( logger.logTryGoToLockedShade(isKeyguard) if (isKeyguard) { val animationHandler: ((Long) -> Unit)? - if (needsQSAnimation) { + if (needsQSAnimation || useSplitShade) { // Let's use the default animation animationHandler = null } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index 56b689efaa79..7d0ac1874056 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -290,7 +290,8 @@ public class NotificationListener extends NotificationListenerWithPlugins implem false, null, 0, - false + false, + 0 ); } return ranking; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 8f9365cd4dc4..99081e98c4a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -65,8 +65,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -74,6 +72,8 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import dagger.Lazy; + /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -465,8 +465,7 @@ public class NotificationRemoteInputManager implements Dumpable { riv.getController().setRemoteInput(input); riv.getController().setRemoteInputs(inputs); riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); - ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null; - riv.focusAnimated(parent); + riv.focusAnimated(); if (userMessageContent != null) { riv.setEditTextContent(userMessageContent); } 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 14d0d7e032d5..9a65e342478e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -31,6 +31,7 @@ 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.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -61,7 +62,6 @@ import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconList; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags; @@ -280,7 +280,7 @@ public interface CentralSurfacesDependenciesModule { @SysUISingleton static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager, KeyguardStateController keyguardStateController, - Lazy<StatusBarKeyguardViewManager> statusBarKeyguardViewManager, + Lazy<AlternateBouncerInteractor> alternateBouncerInteractor, InteractionJankMonitor interactionJankMonitor) { DialogLaunchAnimator.Callback callback = new DialogLaunchAnimator.Callback() { @Override @@ -300,7 +300,7 @@ public interface CentralSurfacesDependenciesModule { @Override public boolean isShowingAlternateAuthOnUnlock() { - return statusBarKeyguardViewManager.get().canShowAlternateBouncer(); + return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint(); } }; return new DialogLaunchAnimator(callback, interactionJankMonitor); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index d6ad7d0fb8cf..284973976003 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -43,6 +43,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceConfigPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView @@ -60,11 +61,11 @@ import java.util.Optional import java.util.concurrent.Executor import javax.inject.Inject -/** - * Controller for managing the smartspace view on the lockscreen - */ +/** Controller for managing the smartspace view on the lockscreen */ @SysUISingleton -class LockscreenSmartspaceController @Inject constructor( +class LockscreenSmartspaceController +@Inject +constructor( private val context: Context, private val featureFlags: FeatureFlags, private val smartspaceManager: SmartspaceManager, @@ -81,7 +82,8 @@ class LockscreenSmartspaceController @Inject constructor( @Main private val uiExecutor: Executor, @Background private val bgExecutor: Executor, @Main private val handler: Handler, - optionalPlugin: Optional<BcSmartspaceDataPlugin> + optionalPlugin: Optional<BcSmartspaceDataPlugin>, + optionalConfigPlugin: Optional<BcSmartspaceConfigPlugin>, ) { companion object { private const val TAG = "LockscreenSmartspaceController" @@ -89,6 +91,7 @@ class LockscreenSmartspaceController @Inject constructor( private var session: SmartspaceSession? = null private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) + private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null) // Smartspace can be used on multiple displays, such as when the user casts their screen private var smartspaceViews = mutableSetOf<SmartspaceView>() @@ -240,6 +243,7 @@ class LockscreenSmartspaceController @Inject constructor( val ssView = plugin.getView(parent) ssView.setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD) ssView.registerDataProvider(plugin) + ssView.registerConfigProvider(configPlugin) ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) { 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 3072c810d31b..05a9a427870e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -35,14 +35,6 @@ class NotifPipelineFlags @Inject constructor( fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE) - val isStabilityIndexFixEnabled: Boolean by lazy { - featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) - } - - val isSemiStableSortEnabled: Boolean by lazy { - featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) - } - val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy { featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index 84ab0d1190f0..b5fce4163bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -98,13 +98,11 @@ data class ListAttachState private constructor( * This can happen if the entry is removed from a group that was broken up or if the entry was * filtered out during any of the filtering steps. */ - fun detach(includingStableIndex: Boolean) { + fun detach() { parent = null section = null promoter = null - if (includingStableIndex) { - stableIndex = -1 - } + stableIndex = -1 } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 65a21a4e2190..4065b98ab0c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -965,8 +965,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ private void annulAddition(ListEntry entry) { - // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility - entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled()); + entry.getAttachState().detach(); } private void assignSections() { @@ -986,50 +985,10 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void sortListAndGroups() { Trace.beginSection("ShadeListBuilder.sortListAndGroups"); - if (mFlags.isSemiStableSortEnabled()) { - sortWithSemiStableSort(); - } else { - sortWithLegacyStability(); - } + sortWithSemiStableSort(); Trace.endSection(); } - private void sortWithLegacyStability() { - // Sort all groups and the top level list - for (ListEntry entry : mNotifList) { - if (entry instanceof GroupEntry) { - GroupEntry parent = (GroupEntry) entry; - parent.sortChildren(mGroupChildrenComparator); - } - } - mNotifList.sort(mTopLevelComparator); - assignIndexes(mNotifList); - - // Check for suppressed order changes - if (!getStabilityManager().isEveryChangeAllowed()) { - mForceReorderable = true; - boolean isSorted = isShadeSortedLegacy(); - mForceReorderable = false; - if (!isSorted) { - getStabilityManager().onEntryReorderSuppressed(); - } - } - } - - private boolean isShadeSortedLegacy() { - if (!isSorted(mNotifList, mTopLevelComparator)) { - return false; - } - for (ListEntry entry : mNotifList) { - if (entry instanceof GroupEntry) { - if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) { - return false; - } - } - } - return true; - } - private void sortWithSemiStableSort() { // Sort each group's children boolean allSorted = true; @@ -1100,29 +1059,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { sectionMemberIndex = 0; currentSection = section; } - if (mFlags.isStabilityIndexFixEnabled()) { - entry.getAttachState().setStableIndex(sectionMemberIndex++); - if (entry instanceof GroupEntry) { - final GroupEntry parent = (GroupEntry) entry; - final NotificationEntry summary = parent.getSummary(); - if (summary != null) { - summary.getAttachState().setStableIndex(sectionMemberIndex++); - } - for (NotificationEntry child : parent.getChildren()) { - child.getAttachState().setStableIndex(sectionMemberIndex++); - } + entry.getAttachState().setStableIndex(sectionMemberIndex++); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + final NotificationEntry summary = parent.getSummary(); + if (summary != null) { + summary.getAttachState().setStableIndex(sectionMemberIndex++); } - } else { - // This old implementation uses the same index number for the group as the first - // child, and fails to assign an index to the summary. Remove once tested. - entry.getAttachState().setStableIndex(sectionMemberIndex); - if (entry instanceof GroupEntry) { - final GroupEntry parent = (GroupEntry) entry; - for (NotificationEntry child : parent.getChildren()) { - child.getAttachState().setStableIndex(sectionMemberIndex++); - } + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); } - sectionMemberIndex++; } } } @@ -1272,11 +1218,6 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( - getStableOrderIndex(o1), - getStableOrderIndex(o2)); - if (cmp != 0) return cmp; - NotifComparator sectionComparator = getSectionComparator(o1, o2); if (sectionComparator != null) { cmp = sectionComparator.compare(o1, o2); @@ -1301,12 +1242,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( - getStableOrderIndex(o1), - getStableOrderIndex(o2)); - if (cmp != 0) return cmp; - - cmp = Integer.compare( + int cmp = Integer.compare( o1.getRepresentativeEntry().getRanking().getRank(), o2.getRepresentativeEntry().getRanking().getRank()); if (cmp != 0) return cmp; @@ -1317,25 +1253,6 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return cmp; }; - /** - * A flag that is set to true when we want to run the comparators as if all reordering is - * allowed. This is used to check if the list is "out of order" after the sort is complete. - */ - private boolean mForceReorderable = false; - - private int getStableOrderIndex(ListEntry entry) { - if (mForceReorderable) { - // this is used to determine if the list is correctly sorted - return -1; - } - if (getStabilityManager().isEntryReorderingAllowed(entry)) { - // let the stability manager constrain or allow reordering - return -1; - } - // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility - return entry.getPreviousAttachState().getStableIndex(); - } - @Nullable private Integer getStableOrderRank(ListEntry entry) { if (getStabilityManager().isEntryReorderingAllowed(entry)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index fbe88dff07f1..7addc8fe7a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -24,6 +24,7 @@ import android.graphics.Canvas; import android.graphics.Point; import android.util.AttributeSet; import android.util.MathUtils; +import android.view.Choreographer; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; @@ -492,12 +493,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (animationListener != null) { mAppearAnimator.addListener(animationListener); } - if (delay > 0) { - // we need to apply the initial state already to avoid drawn frames in the wrong state - updateAppearAnimationAlpha(); - updateAppearRect(); - mAppearAnimator.setStartDelay(delay); - } + // we need to apply the initial state already to avoid drawn frames in the wrong state + updateAppearAnimationAlpha(); + updateAppearRect(); mAppearAnimator.addListener(new AnimatorListenerAdapter() { private boolean mWasCancelled; @@ -528,7 +526,20 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mWasCancelled = true; } }); - mAppearAnimator.start(); + + // Cache the original animator so we can check if the animation should be started in the + // Choreographer callback. It's possible that the original animator (mAppearAnimator) is + // replaced with a new value before the callback is called. + ValueAnimator cachedAnimator = mAppearAnimator; + // Even when delay=0, starting the animation on the next frame is necessary to avoid jank. + // Not doing so will increase the chances our Animator will be forced to skip a value of + // the animation's progression, causing stutter. + Choreographer.getInstance().postFrameCallbackDelayed( + frameTimeNanos -> { + if (mAppearAnimator == cachedAnimator) { + mAppearAnimator.start(); + } + }, delay); } private int getCujType(boolean isAppearing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index c4961029dc33..b084a765956d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -109,7 +109,7 @@ public class ActivatableNotificationViewController return true; } if (ev.getAction() == MotionEvent.ACTION_UP) { - mView.setLastActionUpTime(SystemClock.uptimeMillis()); + mView.setLastActionUpTime(ev.getEventTime()); } // With a11y, just do nothing. if (mAccessibilityManager.isTouchExplorationEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 9f50aef6de11..9275e2b603c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -79,6 +79,8 @@ import com.android.internal.widget.CallLayout; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -177,6 +179,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private PeopleNotificationIdentifier mPeopleNotificationIdentifier; private Optional<BubblesManager> mBubblesManagerOptional; private MetricsLogger mMetricsLogger; + private FeatureFlags mFeatureFlags; private int mIconTransformContentShift; private int mMaxHeadsUpHeightBeforeN; private int mMaxHeadsUpHeightBeforeP; @@ -277,7 +280,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mChildIsExpanding; private boolean mJustClicked; - private boolean mIconAnimationRunning; + private boolean mAnimationRunning; private boolean mShowNoBackground; private ExpandableNotificationRow mNotificationParent; private OnExpandClickListener mOnExpandClickListener; @@ -451,10 +454,26 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mPublicLayout; } - public void setIconAnimationRunning(boolean running) { - for (NotificationContentView l : mLayouts) { - setIconAnimationRunning(running, l); + /** + * Sets animations running in the layouts of this row, including public, private, and children. + * @param running whether the animations should be started running or stopped. + */ + public void setAnimationRunning(boolean running) { + // Sets animations running in the private/public layouts. + if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE)) { + for (NotificationContentView l : mLayouts) { + if (l != null) { + l.setContentAnimationRunning(running); + setIconAnimationRunning(running, l); + } + } + } else { + for (NotificationContentView l : mLayouts) { + setIconAnimationRunning(running, l); + } } + // For groups summaries with children, we want to set the children containers + // animating as well. if (mIsSummaryWithChildren) { NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper(); if (viewWrapper != null) { @@ -468,12 +487,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); - child.setIconAnimationRunning(running); + child.setAnimationRunning(running); } } - mIconAnimationRunning = running; + mAnimationRunning = running; } + /** + * Starts or stops animations of the icons in all potential content views (regardless of + * whether they're contracted, expanded, etc). + * + * @param running whether to start or stop the icon's animation. + */ private void setIconAnimationRunning(boolean running, NotificationContentView layout) { if (layout != null) { View contractedChild = layout.getContractedChild(); @@ -485,16 +510,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** + * Starts or stops animations of the icon in the provided view's icon and right icon. + * + * @param running whether to start or stop the icon's animation. + * @param child the view with the icon to start or stop. + */ private void setIconAnimationRunningForChild(boolean running, View child) { if (child != null) { ImageView icon = child.findViewById(com.android.internal.R.id.icon); - setIconRunning(icon, running); + setImageViewAnimationRunning(icon, running); ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon); - setIconRunning(rightIcon, running); + setImageViewAnimationRunning(rightIcon, running); } } - private void setIconRunning(ImageView imageView, boolean running) { + /** + * Starts or stops the animation of a provided image view if it's an AnimationDrawable or an + * AnimatedVectorDrawable. + * + * @param imageView the image view on which to start/stop animation. + * @param running whether to start or stop the view's animation. + */ + private void setImageViewAnimationRunning(ImageView imageView, boolean running) { if (imageView != null) { Drawable drawable = imageView.getDrawable(); if (drawable instanceof AnimationDrawable) { @@ -561,8 +599,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation()); mChildrenContainer.onNotificationUpdated(); } - if (mIconAnimationRunning) { - setIconAnimationRunning(true); + if (mAnimationRunning) { + setAnimationRunning(true); } if (mLastChronometerRunning) { setChronometerRunning(true); @@ -1038,7 +1076,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView notifyHeightChanged(false /* needsAnimation */); } if (pinned) { - setIconAnimationRunning(true); + setAnimationRunning(true); mExpandedWhenPinned = false; } else if (mExpandedWhenPinned) { setUserExpanded(true); @@ -1627,6 +1665,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView ); } + /** + * Constructs an ExpandableNotificationRow. + * @param context context passed to image resolver + * @param attrs attributes used to initialize parent view + */ public ExpandableNotificationRow(Context context, AttributeSet attrs) { super(context, attrs); mImageResolver = new NotificationInlineImageResolver(context, @@ -1662,7 +1705,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationGutsManager gutsManager, MetricsLogger metricsLogger, SmartReplyConstants smartReplyConstants, - SmartReplyController smartReplyController) { + SmartReplyController smartReplyController, + FeatureFlags featureFlags) { mEntry = entry; mAppName = appName; if (mMenuRow == null) { @@ -1697,6 +1741,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mBubblesManagerOptional = bubblesManagerOptional; mNotificationGutsManager = gutsManager; mMetricsLogger = metricsLogger; + mFeatureFlags = featureFlags; } private void initDimens() { @@ -3588,11 +3633,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @VisibleForTesting protected void setPrivateLayout(NotificationContentView privateLayout) { mPrivateLayout = privateLayout; + mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout}; } @VisibleForTesting protected void setPublicLayout(NotificationContentView publicLayout) { mPublicLayout = publicLayout; + mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout}; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index d1138608805b..bb92dfcdfcb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -219,7 +219,8 @@ public class ExpandableNotificationRowController implements NotifViewController mNotificationGutsManager, mMetricsLogger, mSmartReplyConstants, - mSmartReplyController + mSmartReplyController, + mFeatureFlags ); mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); if (mAllowLongPress) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java index 49dc6550a51f..21f4cb566e28 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java @@ -16,15 +16,22 @@ package com.android.systemui.statusbar.notification.row; +import android.annotation.ColorInt; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.view.View; +import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -41,6 +48,11 @@ public class FooterView extends StackScrollerDecorView { private String mManageNotificationText; private String mManageNotificationHistoryText; + // Footer label + private TextView mSeenNotifsFooterTextView; + private @StringRes int mSeenNotifsFilteredText; + private int mUnlockIconSize; + public FooterView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -73,10 +85,41 @@ public class FooterView extends StackScrollerDecorView { super.onFinishInflate(); mClearAllButton = (FooterViewButton) findSecondaryView(); mManageButton = findViewById(R.id.manage_text); + mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer); updateResources(); updateText(); } + public void setFooterLabelTextAndIcon(@StringRes int text, @DrawableRes int icon) { + mSeenNotifsFilteredText = text; + if (mSeenNotifsFilteredText != 0) { + mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText); + } else { + mSeenNotifsFooterTextView.setText(null); + } + Drawable drawable; + if (icon == 0) { + drawable = null; + } else { + drawable = getResources().getDrawable(icon); + drawable.setBounds(0, 0, mUnlockIconSize, mUnlockIconSize); + } + mSeenNotifsFooterTextView.setCompoundDrawablesRelative(drawable, null, null, null); + updateFooterVisibilityMode(); + } + + private void updateFooterVisibilityMode() { + if (mSeenNotifsFilteredText != 0) { + mManageButton.setVisibility(View.GONE); + mClearAllButton.setVisibility(View.GONE); + mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); + } else { + mManageButton.setVisibility(View.VISIBLE); + mClearAllButton.setVisibility(View.VISIBLE); + mSeenNotifsFooterTextView.setVisibility(View.GONE); + } + } + public void setManageButtonClickListener(OnClickListener listener) { mManageButton.setOnClickListener(listener); } @@ -135,12 +178,19 @@ public class FooterView extends StackScrollerDecorView { mClearAllButton.setTextColor(textColor); mManageButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background)); mManageButton.setTextColor(textColor); + final @ColorInt int labelTextColor = + Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); + mSeenNotifsFooterTextView.setTextColor(labelTextColor); + mSeenNotifsFooterTextView.setCompoundDrawableTintList( + ColorStateList.valueOf(labelTextColor)); } private void updateResources() { mManageNotificationText = getContext().getString(R.string.manage_notifications_text); mManageNotificationHistoryText = getContext() .getString(R.string.manage_notifications_history_text); + mUnlockIconSize = getResources() + .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index e46bf522acff..4a023c41388e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -184,6 +184,8 @@ public class NotificationContentView extends FrameLayout implements Notification private boolean mRemoteInputVisible; private int mUnrestrictedContentHeight; + private boolean mContentAnimating; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); @@ -2129,8 +2131,49 @@ public class NotificationContentView extends FrameLayout implements Notification return false; } + /** + * Starts and stops animations in the underlying views. + * Avoids restarting the animations by checking whether they're already running first. + * Return value is used for testing. + * + * @param running whether to start animations running, or stop them. + * @return true if the state of animations changed. + */ + public boolean setContentAnimationRunning(boolean running) { + boolean stateChangeRequired = (running != mContentAnimating); + if (stateChangeRequired) { + // Starts or stops the animations in the potential views. + if (mContractedWrapper != null) { + mContractedWrapper.setAnimationsRunning(running); + } + if (mExpandedWrapper != null) { + mExpandedWrapper.setAnimationsRunning(running); + } + if (mHeadsUpWrapper != null) { + mHeadsUpWrapper.setAnimationsRunning(running); + } + // Updates the state tracker. + mContentAnimating = running; + return true; + } + return false; + } + private static class RemoteInputViewData { @Nullable RemoteInputView mView; @Nullable RemoteInputViewController mController; } + + @VisibleForTesting + protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) { + mContractedWrapper = contractedWrapper; + } + @VisibleForTesting + protected void setExpandedWrapper(NotificationViewWrapper expandedWrapper) { + mExpandedWrapper = expandedWrapper; + } + @VisibleForTesting + protected void setHeadsUpWrapper(NotificationViewWrapper headsUpWrapper) { + mHeadsUpWrapper = headsUpWrapper; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java index 8732696dc7a1..175ba15eebae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java @@ -18,11 +18,15 @@ package com.android.systemui.statusbar.notification.row.wrapper; import android.app.Notification; import android.content.Context; +import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.service.notification.StatusBarNotification; import android.view.View; +import com.android.internal.R; +import com.android.internal.widget.BigPictureNotificationImageView; import com.android.systemui.statusbar.notification.ImageTransformState; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -31,6 +35,8 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow */ public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper { + private BigPictureNotificationImageView mImageView; + protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); @@ -39,9 +45,14 @@ public class NotificationBigPictureTemplateViewWrapper extends NotificationTempl @Override public void onContentUpdated(ExpandableNotificationRow row) { super.onContentUpdated(row); + resolveViews(); updateImageTag(row.getEntry().getSbn()); } + private void resolveViews() { + mImageView = mView.findViewById(R.id.big_picture); + } + private void updateImageTag(StatusBarNotification sbn) { final Bundle extras = sbn.getNotification().extras; Icon bigLargeIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG, Icon.class); @@ -54,4 +65,25 @@ public class NotificationBigPictureTemplateViewWrapper extends NotificationTempl mRightIcon.setTag(ImageTransformState.ICON_TAG, getLargeIcon(sbn.getNotification())); } } + + /** + * Starts or stops the animations in any drawables contained in this BigPicture Notification. + * + * @param running Whether the animations should be set to run. + */ + @Override + public void setAnimationsRunning(boolean running) { + if (mImageView == null) { + return; + } + Drawable d = mImageView.getDrawable(); + if (d instanceof AnimatedImageDrawable) { + AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d; + if (running) { + animatedImageDrawable.start(); + } else { + animatedImageDrawable.stop(); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index e136055b80b3..10753f215103 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -17,16 +17,20 @@ package com.android.systemui.statusbar.notification.row.wrapper import android.content.Context +import android.graphics.drawable.AnimatedImageDrawable import android.view.View import android.view.ViewGroup import com.android.internal.widget.CachingIconView import com.android.internal.widget.ConversationLayout +import com.android.internal.widget.MessagingGroup +import com.android.internal.widget.MessagingImageMessage import com.android.internal.widget.MessagingLinearLayout import com.android.systemui.R import com.android.systemui.statusbar.notification.NotificationFadeAware import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform +import com.android.systemui.util.children /** * Wraps a notification containing a conversation template @@ -49,6 +53,7 @@ class NotificationConversationTemplateViewWrapper constructor( private lateinit var expandBtn: View private lateinit var expandBtnContainer: View private lateinit var imageMessageContainer: ViewGroup + private lateinit var messageContainers: ArrayList<MessagingGroup> private lateinit var messagingLinearLayout: MessagingLinearLayout private lateinit var conversationTitleView: View private lateinit var importanceRing: View @@ -60,6 +65,7 @@ class NotificationConversationTemplateViewWrapper constructor( private fun resolveViews() { messagingLinearLayout = conversationLayout.messagingLinearLayout imageMessageContainer = conversationLayout.imageMessageContainer + messageContainers = conversationLayout.messagingGroups with(conversationLayout) { conversationIconContainer = requireViewById(com.android.internal.R.id.conversation_icon_container) @@ -146,4 +152,26 @@ class NotificationConversationTemplateViewWrapper constructor( NotificationFadeAware.setLayerTypeForFaded(expandBtn, faded) NotificationFadeAware.setLayerTypeForFaded(conversationIconContainer, faded) } + + // Starts or stops the animations in any drawables contained in this Conversation Notification. + override fun setAnimationsRunning(running: Boolean) { + // We apply to both the child message containers in a conversation group, + // and the top level image message container. + val containers = messageContainers.asSequence().map { it.messageContainer } + + sequenceOf(imageMessageContainer) + val drawables = + containers + .flatMap { it.children } + .mapNotNull { child -> + (child as? MessagingImageMessage)?.let { imageMessage -> + imageMessage.drawable as? AnimatedImageDrawable + } + } + drawables.toSet().forEach { + when { + running -> it.start() + !running -> it.stop() + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java index c587ce05b13f..4592fde69a93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java @@ -17,9 +17,13 @@ package com.android.systemui.statusbar.notification.row.wrapper; import android.content.Context; +import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; +import com.android.internal.widget.MessagingGroup; +import com.android.internal.widget.MessagingImageMessage; import com.android.internal.widget.MessagingLayout; import com.android.internal.widget.MessagingLinearLayout; import com.android.systemui.R; @@ -127,4 +131,40 @@ public class NotificationMessagingTemplateViewWrapper extends NotificationTempla } return super.getMinLayoutHeight(); } + + /** + * Starts or stops the animations in any drawables contained in this Messaging Notification. + * + * @param running Whether the animations should be set to run. + */ + @Override + public void setAnimationsRunning(boolean running) { + if (mMessagingLayout == null) { + return; + } + + for (MessagingGroup group : mMessagingLayout.getMessagingGroups()) { + for (int i = 0; i < group.getMessageContainer().getChildCount(); i++) { + View view = group.getMessageContainer().getChildAt(i); + // We only need to set animations in MessagingImageMessages. + if (!(view instanceof MessagingImageMessage)) { + continue; + } + MessagingImageMessage imageMessage = + (com.android.internal.widget.MessagingImageMessage) view; + + // If the drawable isn't an AnimatedImageDrawable, we can't set it to animate. + Drawable d = imageMessage.getDrawable(); + if (!(d instanceof AnimatedImageDrawable)) { + continue; + } + AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d; + if (running) { + animatedImageDrawable.start(); + } else { + animatedImageDrawable.stop(); + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index 1c22f0933236..ff5b9cbf3c23 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -403,4 +403,12 @@ public abstract class NotificationViewWrapper implements TransformableView { NotificationFadeAware.setLayerTypeForFaded(getIcon(), faded); NotificationFadeAware.setLayerTypeForFaded(getExpandButton(), faded); } + + /** + * Starts or stops the animations in any drawables contained in this Notification. + * + * @param running Whether the animations should be set to run. + */ + public void setAnimationsRunning(boolean running) { + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 8d48d738f0f2..9b93d7b9e1d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1431,6 +1431,22 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; + if (mUseRoundnessSourceTypes) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + } for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ca1e397f930a..aab36da66bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -538,6 +538,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private NotificationStackScrollLayoutController.TouchHandler mTouchHandler; private final ScreenOffAnimationController mScreenOffAnimationController; private boolean mShouldUseSplitNotificationShade; + private boolean mHasFilteredOutSeenNotifications; private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener = new ExpandableView.OnHeightChangedListener() { @@ -684,6 +685,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable updateFooter(); } + void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) { + mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications; + } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooter() { @@ -1811,9 +1816,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getSystemWindowInsetBottom() - + insets.getInsets(WindowInsets.Type.ime()).bottom; - + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { @@ -2262,7 +2265,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private int getImeInset() { - return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight())); + // The NotificationStackScrollLayout does not extend all the way to the bottom of the + // display. Therefore, subtract that space from the mBottomInset, in order to only include + // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. + return Math.max(0, mBottomInset + - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1])); } /** @@ -2970,12 +2977,19 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable childInGroup = (ExpandableNotificationRow) requestedView; requestedView = requestedRow = childInGroup.getNotificationParent(); } - int position = 0; + final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings; + int position = (int) scrimTopPadding; + int visibleIndex = -1; + ExpandableView lastVisibleChild = null; for (int i = 0; i < getChildCount(); i++) { ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; + if (notGone) visibleIndex++; if (notGone && !child.hasNoContentHeight()) { - if (position != 0) { + if (position != scrimTopPadding) { + if (lastVisibleChild != null) { + position += calculateGapHeight(lastVisibleChild, child, visibleIndex); + } position += mPaddingBetweenElements; } } @@ -2987,6 +3001,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (notGone) { position += getIntrinsicHeight(child); + lastVisibleChild = child; } } return 0; @@ -3122,7 +3137,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void updateAnimationState(boolean running, View child) { if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; - row.setIconAnimationRunning(running); + row.setAnimationRunning(running); } } @@ -4602,13 +4617,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - void updateEmptyShadeView( - boolean visible, boolean areNotificationsHiddenInShade, boolean areSeenNotifsFiltered) { + void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) { mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled); if (areNotificationsHiddenInShade) { updateEmptyShadeView(R.string.dnd_suppressing_shade_text, 0, 0); - } else if (areSeenNotifsFiltered) { + } else if (mHasFilteredOutSeenNotifications) { updateEmptyShadeView( R.string.no_unseen_notif_text, R.string.unlock_to_see_notif_text, @@ -4647,13 +4661,20 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) { - if (mFooterView == null) { + if (mFooterView == null || mNotificationStackSizeCalculator == null) { return; } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); mFooterView.setSecondaryVisible(showDismissView, animate); mFooterView.showHistory(showHistory); + if (mHasFilteredOutSeenNotifications) { + mFooterView.setFooterLabelTextAndIcon( + R.string.unlock_to_see_notif_text, + R.drawable.ic_friction_lock_closed); + } else { + mFooterView.setFooterLabelTextAndIcon(0, 0); + } } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 58919489496d..971dce89cf24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1242,11 +1242,7 @@ public class NotificationStackScrollLayoutController { // For more details, see: b/228790482 && !isInTransitionToKeyguard(); - mView.updateEmptyShadeView( - shouldShow, - mZenModeController.areNotificationsHiddenInShade(), - mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard() - && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications()); + mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade()); Trace.endSection(); } @@ -1942,6 +1938,9 @@ public class NotificationStackScrollLayoutController { @Override public void setNotifStats(@NonNull NotifStats notifStats) { mNotifStats = notifStats; + mView.setHasFilteredOutSeenNotifications( + mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard() + && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications()); updateFooter(); updateShowEmptyShadeView(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java index 3ccef9d6eb14..eb81c46027e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java @@ -16,25 +16,35 @@ package com.android.systemui.statusbar.phone; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; + import android.content.Context; import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.view.IWindowManager; import android.view.MotionEvent; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.AutoHideUiElement; +import java.io.PrintWriter; + import javax.inject.Inject; /** A controller to control all auto-hide things. Also see {@link AutoHideUiElement}. */ @SysUISingleton public class AutoHideController { private static final String TAG = "AutoHideController"; - private static final long AUTO_HIDE_TIMEOUT_MS = 2250; + private static final int AUTO_HIDE_TIMEOUT_MS = 2250; + private static final int USER_AUTO_HIDE_TIMEOUT_MS = 350; + private final AccessibilityManager mAccessibilityManager; private final IWindowManager mWindowManagerService; private final Handler mHandler; @@ -52,11 +62,12 @@ public class AutoHideController { }; @Inject - public AutoHideController(Context context, @Main Handler handler, + public AutoHideController(Context context, + @Main Handler handler, IWindowManager iWindowManager) { + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mHandler = handler; mWindowManagerService = iWindowManager; - mDisplayId = context.getDisplayId(); } @@ -138,7 +149,12 @@ public class AutoHideController { private void scheduleAutoHide() { cancelAutoHide(); - mHandler.postDelayed(mAutoHide, AUTO_HIDE_TIMEOUT_MS); + mHandler.postDelayed(mAutoHide, getAutoHideTimeout()); + } + + private int getAutoHideTimeout() { + return mAccessibilityManager.getRecommendedTimeoutMillis(AUTO_HIDE_TIMEOUT_MS, + FLAG_CONTENT_CONTROLS); } public void checkUserAutoHide(MotionEvent event) { @@ -160,7 +176,13 @@ public class AutoHideController { private void userAutoHide() { cancelAutoHide(); - mHandler.postDelayed(mAutoHide, 350); // longer than app gesture -> flag clear + // longer than app gesture -> flag clear + mHandler.postDelayed(mAutoHide, getUserAutoHideTimeout()); + } + + private int getUserAutoHideTimeout() { + return mAccessibilityManager.getRecommendedTimeoutMillis(USER_AUTO_HIDE_TIMEOUT_MS, + FLAG_CONTENT_CONTROLS); } private boolean isAnyTransientBarShown() { @@ -175,6 +197,15 @@ public class AutoHideController { return false; } + public void dump(@NonNull PrintWriter pw) { + pw.println("AutoHideController:"); + pw.println("\tmAutoHideSuspended=" + mAutoHideSuspended); + pw.println("\tisAnyTransientBarShown=" + isAnyTransientBarShown()); + pw.println("\thasPendingAutoHide=" + mHandler.hasCallbacks(mAutoHide)); + pw.println("\tgetAutoHideTimeout=" + getAutoHideTimeout()); + pw.println("\tgetUserAutoHideTimeout=" + getUserAutoHideTimeout()); + } + /** * Injectable factory for creating a {@link AutoHideController}. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 9070eadd9944..149ec545dfa7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -154,9 +154,7 @@ public class AutoTileManager implements UserAwareController { if (!mAutoTracker.isAdded(SAVER)) { mDataSaverController.addCallback(mDataSaverListener); } - if (!mAutoTracker.isAdded(WORK)) { - mManagedProfileController.addCallback(mProfileCallback); - } + mManagedProfileController.addCallback(mProfileCallback); if (!mAutoTracker.isAdded(NIGHT) && ColorDisplayManager.isNightDisplayAvailable(mContext)) { mNightDisplayListener.setCallback(mNightDisplayCallback); @@ -275,18 +273,18 @@ public class AutoTileManager implements UserAwareController { return mCurrentUser.getIdentifier(); } - public void unmarkTileAsAutoAdded(String tabSpec) { - mAutoTracker.setTileRemoved(tabSpec); - } - private final ManagedProfileController.Callback mProfileCallback = new ManagedProfileController.Callback() { @Override public void onManagedProfileChanged() { - if (mAutoTracker.isAdded(WORK)) return; if (mManagedProfileController.hasActiveProfile()) { + if (mAutoTracker.isAdded(WORK)) return; mHost.addTile(WORK); mAutoTracker.setTileAdded(WORK); + } else { + if (!mAutoTracker.isAdded(WORK)) return; + mHost.removeTile(WORK); + mAutoTracker.setTileRemoved(WORK); } } @@ -429,7 +427,7 @@ public class AutoTileManager implements UserAwareController { initSafetyTile(); } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) { mHost.removeTile(mSafetySpec); - mHost.unmarkTileAsAutoAdded(mSafetySpec); + mAutoTracker.setTileRemoved(mSafetySpec); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 895a2934ec1b..db2c0a08c1d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; + import android.annotation.IntDef; import android.content.res.Resources; import android.hardware.biometrics.BiometricFaceConstants; @@ -27,7 +29,6 @@ import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; -import android.os.SystemClock; import android.os.Trace; import androidx.annotation.Nullable; @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -78,6 +80,7 @@ import javax.inject.Inject; */ @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { + private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -169,9 +172,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final MetricsLogger mMetricsLogger; private final AuthController mAuthController; private final StatusBarStateController mStatusBarStateController; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final LatencyTracker mLatencyTracker; private final VibratorHelper mVibratorHelper; private final BiometricUnlockLogger mLogger; + private final SystemClock mSystemClock; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -279,14 +284,17 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp SessionTracker sessionTracker, LatencyTracker latencyTracker, ScreenOffAnimationController screenOffAnimationController, - VibratorHelper vibrator) { + VibratorHelper vibrator, + SystemClock systemClock + ) { mPowerManager = powerManager; mShadeController = shadeController; mUpdateMonitor = keyguardUpdateMonitor; mUpdateMonitor.registerCallback(this); mMediaManager = notificationMediaManager; mLatencyTracker = latencyTracker; - wakefulnessLifecycle.addObserver(mWakefulnessObserver); + mWakefulnessLifecycle = wakefulnessLifecycle; + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); screenLifecycle.addObserver(mScreenObserver); mNotificationShadeWindowController = notificationShadeWindowController; @@ -306,6 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mScreenOffAnimationController = screenOffAnimationController; mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getName(), this); } @@ -429,8 +438,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, - "android.policy:BIOMETRIC"); + mPowerManager.wakeUp( + mSystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_BIOMETRIC, + "android.policy:BIOMETRIC" + ); } Trace.beginSection("release wake-and-unlock"); releaseBiometricWakeLock(); @@ -670,7 +682,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp startWakeAndUnlock(MODE_ONLY_WAKE); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT && mUpdateMonitor.isUdfpsSupported()) { - long currUptimeMillis = SystemClock.uptimeMillis(); + long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; } else { @@ -718,12 +730,26 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - //these haptics are for device-entry only + // these haptics are for device-entry only private void vibrateSuccess(BiometricSourceType type) { + if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) + && lastWakeupFromPowerButtonWithinHapticThreshold()) { + mLogger.d("Skip auth success haptic. Power button was recently pressed."); + return; + } mVibratorHelper.vibrateAuthSuccess( getClass().getSimpleName() + ", type =" + type + "device-entry::success"); } + private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { + final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON; + return lastWakeupFromPowerButton + && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME + && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() + < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; + } + private void vibrateError(BiometricSourceType type) { mVibratorHelper.vibrateAuthError( getClass().getSimpleName() + ", type =" + type + "device-entry::error"); @@ -816,7 +842,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mUpdateMonitor.isUdfpsSupported()) { pw.print(" mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures); pw.print(" time since last failure="); - pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); + pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); } } 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 688ce7b94b56..4a3a2d059b9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -64,7 +64,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; @@ -162,6 +161,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; @@ -291,26 +291,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl(); - /** - * If true, the system is in the half-boot-to-decryption-screen state. - * Prudently disable QS and notifications. - */ - public static final boolean ONLY_CORE_APPS; - - static { - boolean onlyCoreApps; - try { - IPackageManager packageManager = - IPackageManager.Stub.asInterface(ServiceManager.getService("package")); - onlyCoreApps = packageManager != null && packageManager.isOnlyCoreApps(); - } catch (RemoteException e) { - onlyCoreApps = false; - } - ONLY_CORE_APPS = onlyCoreApps; - } - private final Context mContext; private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; + private final DeviceStateManager mDeviceStateManager; private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks; private float mTransitionToFullShadeProgress = 0f; private NotificationListContainer mNotifListContainer; @@ -485,6 +468,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final ShadeController mShadeController; private final InitController mInitController; private final Lazy<CameraLauncher> mCameraLauncherLazy; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final PluginDependencyProvider mPluginDependencyProvider; private final KeyguardDismissUtil mKeyguardDismissUtil; @@ -763,7 +747,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { WiredChargingRippleController wiredChargingRippleController, IDreamManager dreamManager, Lazy<CameraLauncher> cameraLauncherLazy, - Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy) { + Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy, + AlternateBouncerInteractor alternateBouncerInteractor + ) { mContext = context; mNotificationsController = notificationsController; mFragmentService = fragmentService; @@ -841,6 +827,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mWallpaperManager = wallpaperManager; mJankMonitor = jankMonitor; mCameraLauncherLazy = cameraLauncherLazy; + mAlternateBouncerInteractor = alternateBouncerInteractor; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -874,8 +861,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT, id -> onLaunchTransitionTimeout()); - deviceStateManager.registerCallback(mMainExecutor, - new FoldStateListener(mContext, this::onFoldedStateChanged)); + mDeviceStateManager = deviceStateManager; wiredChargingRippleController.registerCallbacks(); mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy; @@ -1064,6 +1050,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } }); + registerCallbacks(); + mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener); mPluginManager.addPluginListener( @@ -1119,6 +1107,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @VisibleForTesting + /** Registers listeners/callbacks with external dependencies. */ + void registerCallbacks() { + //TODO(b/264502026) move the rest of the listeners here. + mDeviceStateManager.registerCallback(mMainExecutor, + new FoldStateListener(mContext, this::onFoldedStateChanged)); + } + + @VisibleForTesting void initShadeVisibilityListener() { mShadeController.setVisibilityListener(new ShadeController.ShadeVisibilityListener() { @Override @@ -1687,8 +1683,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { || !mUserSwitcherController.isSimpleUserSwitcher()) && !isShadeDisabled() && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0) - && !mDozing - && !ONLY_CORE_APPS; + && !mDozing; mNotificationPanelViewController.setQsExpansionEnabledPolicy(expandEnabled); Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled); } @@ -3257,8 +3252,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void showBouncerOrLockScreenIfKeyguard() { // If the keyguard is animating away, we aren't really the keyguard anymore and should not // show the bouncer/lockscreen. - if (!mKeyguardViewMediator.isHiding() - && !mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { + if (!mKeyguardViewMediator.isHiding() && !mKeyguardUpdateMonitor.isKeyguardGoingAway()) { if (mState == StatusBarState.SHADE_LOCKED) { // shade is showing while locked on the keyguard, so go back to showing the // lock screen where users can use the UDFPS affordance to enter the device @@ -3738,7 +3732,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean launchingAffordanceWithPreview = mLaunchingAffordance; mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview); - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED || mTransitionToFullShadeProgress > 0f) { mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE); @@ -4263,8 +4257,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onDozeAmountChanged(float linear, float eased) { - if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) + if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index de7b152adaab..0446cefb10dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -44,10 +44,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; @@ -82,7 +81,6 @@ public class DozeParameters implements private final AlwaysOnDisplayPolicy mAlwaysOnPolicy; private final Resources mResources; private final BatteryController mBatteryController; - private final FeatureFlags mFeatureFlags; private final ScreenOffAnimationController mScreenOffAnimationController; private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @@ -125,7 +123,6 @@ public class DozeParameters implements BatteryController batteryController, TunerService tunerService, DumpManager dumpManager, - FeatureFlags featureFlags, ScreenOffAnimationController screenOffAnimationController, Optional<SysUIUnfoldComponent> sysUiUnfoldComponent, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, @@ -141,7 +138,6 @@ public class DozeParameters implements mControlScreenOffAnimation = !getDisplayNeedsBlanking(); mPowerManager = powerManager; mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation); - mFeatureFlags = featureFlags; mScreenOffAnimationController = screenOffAnimationController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; @@ -162,6 +158,13 @@ public class DozeParameters implements SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); quickPickupSettingsObserver.observe(); + + batteryController.addCallback(new BatteryStateChangeCallback() { + @Override + public void onPowerSaveChanged(boolean isPowerSave) { + dispatchAlwaysOnEvent(); + } + }); } private void updateQuickPickupEnabled() { @@ -300,13 +303,10 @@ public class DozeParameters implements /** * Whether we're capable of controlling the screen off animation if we want to. This isn't - * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs - * blanking. + * possible if AOD isn't even enabled or if the display needs blanking. */ public boolean canControlUnlockedScreenOff() { - return getAlwaysOn() - && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !getDisplayNeedsBlanking(); + return getAlwaysOn() && !getDisplayNeedsBlanking(); } /** @@ -424,9 +424,7 @@ public class DozeParameters implements updateControlScreenOff(); } - for (Callback callback : mCallbacks) { - callback.onAlwaysOnChange(); - } + dispatchAlwaysOnEvent(); mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @@ -463,6 +461,12 @@ public class DozeParameters implements pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled()); } + private void dispatchAlwaysOnEvent() { + for (Callback callback : mCallbacks) { + callback.onAlwaysOnChange(); + } + } + private boolean getPostureSpecificBool( int[] postureMapping, boolean defaultSensorBool, @@ -477,7 +481,8 @@ public class DozeParameters implements return bool; } - interface Callback { + /** Callbacks for doze parameter related information */ + public interface Callback { /** * Invoked when the value of getAlwaysOn may have changed. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 000fe140882c..d31875935dd3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.phone; import static com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; import android.content.Context; @@ -44,6 +46,8 @@ import com.android.systemui.DejankUtils; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.DismissCallbackRegistry; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.KeyguardResetCallback; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.ListenerSet; @@ -64,14 +68,6 @@ public class KeyguardBouncer { private static final String TAG = "PrimaryKeyguardBouncer"; static final long BOUNCER_FACE_DELAY = 1200; public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f; - /** - * Values for the bouncer expansion represented as the panel expansion. - * Panel expansion 1f = panel fully showing = bouncer fully hidden - * Panel expansion 0f = panel fully hiding = bouncer fully showing - */ - public static final float EXPANSION_HIDDEN = 1f; - public static final float EXPANSION_VISIBLE = 0f; - protected final Context mContext; protected final ViewMediatorCallback mCallback; protected final ViewGroup mContainer; @@ -664,56 +660,6 @@ public class KeyguardBouncer { mExpansionCallbacks.remove(callback); } - /** - * Callback updated when the primary bouncer's show and hide states change. - */ - public interface PrimaryBouncerExpansionCallback { - /** - * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_VISIBLE}. - * This is NOT called each time the bouncer is shown, but rather only when the fully - * shown amount has changed based on the panel expansion. The bouncer's visibility - * can still change when the expansion amount hasn't changed. - * See {@link KeyguardBouncer#isShowing()} for the checks for the bouncer showing state. - */ - default void onFullyShown() { - } - - /** - * Invoked when the bouncer is starting to transition to a hidden state. - */ - default void onStartingToHide() { - } - - /** - * Invoked when the bouncer is starting to transition to a visible state. - */ - default void onStartingToShow() { - } - - /** - * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_HIDDEN}. - */ - default void onFullyHidden() { - } - - /** - * From 0f {@link KeyguardBouncer#EXPANSION_VISIBLE} when fully visible - * to 1f {@link KeyguardBouncer#EXPANSION_HIDDEN} when fully hidden - */ - default void onExpansionChanged(float bouncerHideAmount) {} - - /** - * Invoked when visibility of KeyguardBouncer has changed. - * Note the bouncer expansion can be {@link KeyguardBouncer#EXPANSION_VISIBLE}, but the - * view's visibility can be {@link View.INVISIBLE}. - */ - default void onVisibilityChanged(boolean isVisible) {} - } - - public interface KeyguardResetCallback { - void onKeyguardReset(); - } - /** Create a {@link KeyguardBouncer} once a container and bouncer callback are available. */ public static class Factory { private final Context mContext; 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 b965ac97cc1c..ff1b31d8848f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -30,6 +30,9 @@ 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 +import com.android.systemui.statusbar.policy.DevicePostureController +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN +import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.tuner.TunerService import java.io.PrintWriter @@ -40,11 +43,19 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr private val mKeyguardStateController: KeyguardStateController private val statusBarStateController: StatusBarStateController + private val devicePostureController: DevicePostureController @BypassOverride private val bypassOverride: Int private var hasFaceFeature: Boolean + @DevicePostureInt private val configFaceAuthSupportedPosture: Int + @DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN private var pendingUnlock: PendingUnlock? = null private val listeners = mutableListOf<OnBypassStateChangedListener>() - + private val postureCallback = DevicePostureController.Callback { posture -> + if (postureState != posture) { + postureState = posture + notifyListeners() + } + } private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback { override fun onFaceAuthEnabledChanged() = notifyListeners() } @@ -86,7 +97,8 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr FACE_UNLOCK_BYPASS_NEVER -> false else -> field } - return enabled && mKeyguardStateController.isFaceAuthEnabled + return enabled && mKeyguardStateController.isFaceAuthEnabled && + isPostureAllowedForFaceAuth() } private set(value) { field = value @@ -106,18 +118,31 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr lockscreenUserManager: NotificationLockscreenUserManager, keyguardStateController: KeyguardStateController, shadeExpansionStateManager: ShadeExpansionStateManager, + devicePostureController: DevicePostureController, dumpManager: DumpManager ) { this.mKeyguardStateController = keyguardStateController this.statusBarStateController = statusBarStateController + this.devicePostureController = devicePostureController bypassOverride = context.resources.getInteger(R.integer.config_face_unlock_bypass_override) + configFaceAuthSupportedPosture = + context.resources.getInteger(R.integer.config_face_auth_supported_posture) - hasFaceFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE) + hasFaceFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE) if (!hasFaceFeature) { return } + if (configFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) { + devicePostureController.addCallback { posture -> + if (postureState != posture) { + postureState = posture + notifyListeners() + } + } + } + dumpManager.registerDumpable("KeyguardBypassController", this) statusBarStateController.addCallback(object : StatusBarStateController.StateListener { override fun onStateChanged(newState: Int) { @@ -203,6 +228,13 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr pendingUnlock = null } + fun isPostureAllowedForFaceAuth(): Boolean { + return when (configFaceAuthSupportedPosture) { + DEVICE_POSTURE_UNKNOWN -> true + else -> (postureState == configFaceAuthSupportedPosture) + } + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("KeyguardBypassController:") if (pendingUnlock != null) { @@ -219,6 +251,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr pw.println(" launchingAffordance: $launchingAffordance") pw.println(" qSExpanded: $qsExpanded") pw.println(" hasFaceFeature: $hasFaceFeature") + pw.println(" postureState: $postureState") } /** Registers a listener for bypass state changes. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 348357445223..4ad319969eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.CommandQueue; @@ -76,6 +77,7 @@ import javax.inject.Inject; /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> { + private static final String TAG = "KeyguardStatusBarViewController"; private static final AnimationProperties KEYGUARD_HUN_PROPERTIES = new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -422,7 +424,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { - mLogger.d("animating status bar in"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. return; @@ -438,7 +440,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { - mLogger.d("animating status bar out"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); anim.setStartDelay(startDelay); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index ee8b86160f51..f78472386006 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -53,6 +53,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -147,7 +148,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump * 0, the bouncer is visible. */ @FloatRange(from = 0, to = 1) - private float mBouncerHiddenFraction = KeyguardBouncer.EXPANSION_HIDDEN; + private float mBouncerHiddenFraction = KeyguardBouncerConstants.EXPANSION_HIDDEN; /** * Set whether an unocclusion animation is currently running on the notification panel. Used @@ -810,7 +811,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } if (mState == ScrimState.DREAMING - && mBouncerHiddenFraction != KeyguardBouncer.EXPANSION_HIDDEN) { + && mBouncerHiddenFraction != KeyguardBouncerConstants.EXPANSION_HIDDEN) { final float interpolatedFraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress( mBouncerHiddenFraction); 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 1a14a0363763..24ad55d67bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -79,12 +79,30 @@ public interface StatusBarIconController { /** Refresh the state of an IconManager by recreating the views */ void refreshIconGroup(IconManager iconManager); - /** */ + + /** + * Adds or updates an icon for a given slot for a **tile service icon**. + * + * TODO(b/265307726): Merge with {@link #setIcon(String, StatusBarIcon)} or make this method + * much more clearly distinct from that method. + */ void setExternalIcon(String slot); - /** */ + + /** + * Adds or updates an icon for the given slot for **internal system icons**. + * + * TODO(b/265307726): Rename to `setInternalIcon`, or merge this appropriately with the + * {@link #setIcon(String, StatusBarIcon)} method. + */ void setIcon(String slot, int resourceId, CharSequence contentDescription); - /** */ + + /** + * Adds or updates an icon for the given slot for an **externally-provided icon**. + * + * TODO(b/265307726): Rename to `setExternalIcon` or something similar. + */ void setIcon(String slot, StatusBarIcon icon); + /** */ void setWifiIcon(String slot, WifiIconState state); @@ -133,9 +151,17 @@ public interface StatusBarIconController { * TAG_PRIMARY to refer to the first icon at a given slot. */ void removeIcon(String slot, int tag); + /** */ void removeAllIconsForSlot(String slot); + /** + * Removes all the icons for the given slot. + * + * Only use this for icons that have come from **an external process**. + */ + void removeAllIconsForExternalSlot(String slot); + // TODO: See if we can rename this tunable name. String ICON_HIDE_LIST = "icon_blacklist"; 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 9fbe6cbc0e32..416bc7141eeb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -28,6 +28,8 @@ import android.util.ArraySet; import android.util.Log; import android.view.ViewGroup; +import androidx.annotation.VisibleForTesting; + import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.Dumpable; import com.android.systemui.R; @@ -63,6 +65,10 @@ public class StatusBarIconControllerImpl implements Tunable, ConfigurationListener, Dumpable, CommandQueue.Callbacks, StatusBarIconController, DemoMode { private static final String TAG = "StatusBarIconController"; + // Use this suffix to prevent external icon slot names from unintentionally overriding our + // internal, system-level slot names. See b/255428281. + @VisibleForTesting + protected static final String EXTERNAL_SLOT_SUFFIX = "__external"; private final StatusBarIconList mStatusBarIconList; private final ArrayList<IconManager> mIconGroups = new ArrayList<>(); @@ -346,21 +352,26 @@ public class StatusBarIconControllerImpl implements Tunable, @Override public void setExternalIcon(String slot) { - int viewIndex = mStatusBarIconList.getViewIndex(slot, 0); + String slotName = createExternalSlotName(slot); + int viewIndex = mStatusBarIconList.getViewIndex(slotName, 0); int height = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_icon_drawing_size); mIconGroups.forEach(l -> l.onIconExternal(viewIndex, height)); } - //TODO: remove this (used in command queue and for 3rd party tiles?) + // Override for *both* CommandQueue.Callbacks AND StatusBarIconController. + // TODO(b/265307726): Pull out the CommandQueue callbacks into a member variable to + // differentiate between those callback methods and StatusBarIconController methods. + @Override public void setIcon(String slot, StatusBarIcon icon) { + String slotName = createExternalSlotName(slot); if (icon == null) { - removeAllIconsForSlot(slot); + removeAllIconsForSlot(slotName); return; } StatusBarIconHolder holder = StatusBarIconHolder.fromIcon(icon); - setIcon(slot, holder); + setIcon(slotName, holder); } private void setIcon(String slot, @NonNull StatusBarIconHolder holder) { @@ -406,10 +417,12 @@ public class StatusBarIconControllerImpl implements Tunable, } } - /** */ + // CommandQueue.Callbacks override + // TODO(b/265307726): Pull out the CommandQueue callbacks into a member variable to + // differentiate between those callback methods and StatusBarIconController methods. @Override public void removeIcon(String slot) { - removeAllIconsForSlot(slot); + removeAllIconsForExternalSlot(slot); } /** */ @@ -423,6 +436,11 @@ public class StatusBarIconControllerImpl implements Tunable, mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex)); } + @Override + public void removeAllIconsForExternalSlot(String slotName) { + removeAllIconsForSlot(createExternalSlotName(slotName)); + } + /** */ @Override public void removeAllIconsForSlot(String slotName) { @@ -506,4 +524,12 @@ public class StatusBarIconControllerImpl implements Tunable, public void onDensityOrFontScaleChanged() { refreshIconGroups(); } + + private String createExternalSlotName(String slot) { + if (slot.endsWith(EXTERNAL_SLOT_SUFFIX)) { + return slot; + } else { + return slot + EXTERNAL_SLOT_SUFFIX; + } + } } 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 d480fabe75b1..be06b05fc136 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone; import static android.view.WindowInsets.Type.navigationBars; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING; @@ -36,7 +37,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; -import android.window.OnBackInvokedCallback; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; @@ -58,7 +60,9 @@ 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.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; @@ -75,7 +79,6 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.FoldAodAnimationController; @@ -134,6 +137,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController; private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final BouncerView mPrimaryBouncerView; private final Lazy<ShadeController> mShadeController; @@ -182,8 +186,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb isVisible && mDreamOverlayStateController.isOverlayActive()); if (!isVisible) { - mCentralSurfaces.setPrimaryBouncerHiddenFraction( - KeyguardBouncer.EXPANSION_HIDDEN); + mCentralSurfaces.setPrimaryBouncerHiddenFraction(EXPANSION_HIDDEN); } /* Register predictive back callback when keyguard becomes visible, and unregister @@ -196,11 +199,38 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } }; - private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { - if (DEBUG) { - Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()"); + private final OnBackAnimationCallback mOnBackInvokedCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + if (DEBUG) { + Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()"); + } + onBackPressed(); + if (shouldPlayBackAnimation()) { + mPrimaryBouncerView.getDelegate().getBackCallback().onBackInvoked(); + } + } + + @Override + public void onBackProgressed(BackEvent event) { + if (shouldPlayBackAnimation()) { + mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event); + } + } + + @Override + public void onBackCancelled() { + if (shouldPlayBackAnimation()) { + mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled(); + } + } + + @Override + public void onBackStarted(BackEvent event) { + if (shouldPlayBackAnimation()) { + mPrimaryBouncerView.getDelegate().getBackCallback().onBackStarted(event); + } } - onBackPressed(); }; private boolean mIsBackCallbackRegistered = false; @@ -253,6 +283,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); private boolean mIsModernBouncerEnabled; private boolean mIsUnoccludeTransitionFlagEnabled; + private boolean mIsModernAlternateBouncerEnabled; + private boolean mIsBackAnimationEnabled; private OnDismissAction mAfterKeyguardGoneAction; private Runnable mKeyguardGoneCancelAction; @@ -269,7 +301,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final LatencyTracker mLatencyTracker; private final KeyguardSecurityModel mKeyguardSecurityModel; @Nullable private KeyguardBypassController mBypassController; - @Nullable private AlternateBouncer mAlternateBouncer; + @Nullable private OccludingAppBiometricUI mOccludingAppBiometricUI; private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @@ -306,7 +338,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb FeatureFlags featureFlags, PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor, PrimaryBouncerInteractor primaryBouncerInteractor, - BouncerView primaryBouncerView) { + BouncerView primaryBouncerView, + AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mViewMediatorCallback = callback; mLockPatternUtils = lockPatternUtils; @@ -331,6 +364,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER); mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION); + mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER); + mAlternateBouncerInteractor = alternateBouncerInteractor; + mIsBackAnimationEnabled = + featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM); } @Override @@ -363,23 +400,51 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } /** - * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else, + * Sets the given legacy alternate bouncer to null if it's the current alternate bouncer. Else, + * does nothing. Only used if modern alternate bouncer is NOT enabled. + */ + public void removeLegacyAlternateBouncer( + @NonNull LegacyAlternateBouncer alternateBouncerLegacy) { + if (!mIsModernAlternateBouncerEnabled) { + if (Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(), + alternateBouncerLegacy)) { + mAlternateBouncerInteractor.setLegacyAlternateBouncer(null); + hideAlternateBouncer(true); + } + } + } + + /** + * Sets a new legacy alternate bouncer. Only used if mdoern alternate bouncer is NOT enable. + */ + public void setLegacyAlternateBouncer(@NonNull LegacyAlternateBouncer alternateBouncerLegacy) { + if (!mIsModernAlternateBouncerEnabled) { + if (!Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(), + alternateBouncerLegacy)) { + mAlternateBouncerInteractor.setLegacyAlternateBouncer(alternateBouncerLegacy); + hideAlternateBouncer(false); + } + } + + } + + + /** + * Sets the given OccludingAppBiometricUI to null if it's the current auth interceptor. Else, * does nothing. */ - public void removeAlternateAuthInterceptor(@NonNull AlternateBouncer authInterceptor) { - if (Objects.equals(mAlternateBouncer, authInterceptor)) { - mAlternateBouncer = null; - hideAlternateBouncer(true); + public void removeOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) { + if (Objects.equals(mOccludingAppBiometricUI, biometricUI)) { + mOccludingAppBiometricUI = null; } } /** - * Sets a new alt auth interceptor. + * Sets a new OccludingAppBiometricUI. */ - public void setAlternateBouncer(@NonNull AlternateBouncer authInterceptor) { - if (!Objects.equals(mAlternateBouncer, authInterceptor)) { - mAlternateBouncer = authInterceptor; - hideAlternateBouncer(false); + public void setOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) { + if (!Objects.equals(mOccludingAppBiometricUI, biometricUI)) { + mOccludingAppBiometricUI = biometricUI; } } @@ -438,6 +503,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } + private boolean shouldPlayBackAnimation() { + // Suppress back animation when bouncer shouldn't be dismissed on back invocation. + return !needsFullscreenBouncer() && mIsBackAnimationEnabled; + } + @Override public void onDensityOrFontScaleChanged() { hideBouncer(true /* destroyView */); @@ -451,7 +521,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb || mNotificationPanelViewController.isExpanding()); final boolean isUserTrackingStarted = - event.getFraction() != KeyguardBouncer.EXPANSION_HIDDEN && event.getTracking(); + event.getFraction() != EXPANSION_HIDDEN && event.getTracking(); return mKeyguardStateController.isShowing() && !primaryBouncerIsOrWillBeShowing() @@ -501,9 +571,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } else { if (mPrimaryBouncer != null) { - mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + mPrimaryBouncer.setExpansion(EXPANSION_HIDDEN); } else { - mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN); } } } @@ -566,18 +636,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * {@see KeyguardBouncer#show(boolean, boolean)} */ public void showBouncer(boolean scrimmed) { - if (canShowAlternateBouncer()) { - updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer()); - return; + if (!mAlternateBouncerInteractor.show()) { + showPrimaryBouncer(scrimmed); + } else { + updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState()); } - - showPrimaryBouncer(scrimmed); - } - - /** Whether we can show the alternate bouncer instead of the primary bouncer. */ - public boolean canShowAlternateBouncer() { - return mAlternateBouncer != null - && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true); } /** @@ -641,9 +704,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = cancelAction; mDismissActionWillAnimateOnKeyguard = r != null && r.willRunAnimationOnKeyguard(); - // If there is an an alternate auth interceptor (like the UDFPS), show that one + // If there is an alternate auth interceptor (like the UDFPS), show that one // instead of the bouncer. - if (canShowAlternateBouncer()) { + if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) { if (!afterKeyguardGone) { if (mPrimaryBouncer != null) { mPrimaryBouncer.setDismissAction(mAfterKeyguardGoneAction, @@ -656,7 +719,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = null; } - updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer()); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.show()); return; } @@ -725,10 +788,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void hideAlternateBouncer(boolean forceUpdateScrim) { - final boolean updateScrim = (mAlternateBouncer != null - && mAlternateBouncer.hideAlternateBouncer()) - || forceUpdateScrim; - updateAlternateBouncerShowing(updateScrim); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.hide() || forceUpdateScrim); } private void updateAlternateBouncerShowing(boolean updateScrim) { @@ -738,7 +798,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb return; } - final boolean isShowingAlternateBouncer = isShowingAlternateBouncer(); + final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState(); if (mKeyguardMessageAreaController != null) { mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer); mKeyguardMessageAreaController.setMessage(""); @@ -1095,7 +1155,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public boolean isBouncerShowing() { - return primaryBouncerIsShowing() || isShowingAlternateBouncer(); + return primaryBouncerIsShowing() || mAlternateBouncerInteractor.isVisibleState(); } @Override @@ -1339,7 +1399,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth); } - if (mAlternateBouncer != null && isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { hideAlternateBouncer(false); executeAfterKeyguardGoneAction(); } @@ -1347,7 +1407,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb /** Display security message to relevant KeyguardMessageArea. */ public void setKeyguardMessage(String message, ColorStateList colorState) { - if (isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { if (mKeyguardMessageAreaController != null) { mKeyguardMessageAreaController.setMessage(message); } @@ -1421,6 +1481,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void dump(PrintWriter pw) { pw.println("StatusBarKeyguardViewManager:"); + pw.println(" mIsModernAlternateBouncerEnabled: " + mIsModernAlternateBouncerEnabled); pw.println(" mRemoteInputActive: " + mRemoteInputActive); pw.println(" mDozing: " + mDozing); pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction); @@ -1438,9 +1499,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncer.dump(pw); } - if (mAlternateBouncer != null) { - pw.println("AlternateBouncer:"); - mAlternateBouncer.dump(pw); + if (mOccludingAppBiometricUI != null) { + pw.println("mOccludingAppBiometricUI:"); + mOccludingAppBiometricUI.dump(pw); } } @@ -1492,14 +1553,17 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb return mPrimaryBouncer; } - public boolean isShowingAlternateBouncer() { - return mAlternateBouncer != null && mAlternateBouncer.isShowingAlternateBouncer(); - } - /** - * Forward touches to callbacks. + * For any touches on the NPVC, show the primary bouncer if the alternate bouncer is currently + * showing. */ public void onTouch(MotionEvent event) { + if (mAlternateBouncerInteractor.isVisibleState() + && mAlternateBouncerInteractor.hasAlternateBouncerShownWithMinTime()) { + showPrimaryBouncer(true); + } + + // Forward NPVC touches to callbacks in case they want to respond to touches for (KeyguardViewManagerCallback callback: mCallbacks) { callback.onTouch(event); } @@ -1542,8 +1606,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb */ public void requestFp(boolean request, int udfpsColor) { mKeyguardUpdateManager.requestFingerprintAuthOnOccludingApp(request); - if (mAlternateBouncer != null) { - mAlternateBouncer.requestUdfps(request, udfpsColor); + if (mOccludingAppBiometricUI != null) { + mOccludingAppBiometricUI.requestUdfps(request, udfpsColor); } } @@ -1614,10 +1678,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } /** - * Delegate used to send show and hide events to an alternate authentication method instead of - * the regular pin/pattern/password bouncer. + * @Deprecated Delegate used to send show and hide events to an alternate bouncer. */ - public interface AlternateBouncer { + public interface LegacyAlternateBouncer { /** * Show alternate authentication bouncer. * @return whether alternate auth method was newly shown @@ -1634,7 +1697,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return true if the alternate auth bouncer is showing */ boolean isShowingAlternateBouncer(); + } + /** + * Delegate used to send show and hide events to an alternate authentication method instead of + * the regular pin/pattern/password bouncer. + */ + public interface OccludingAppBiometricUI { /** * Use when an app occluding the keyguard would like to give the user ability to * unlock the device using udfps. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index 6cd8c78dd52f..9e6bb20f429d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.phone +import android.view.InsetsFlags import android.view.InsetsVisibilities +import android.view.ViewDebug import android.view.WindowInsetsController.Appearance import android.view.WindowInsetsController.Behavior import com.android.internal.statusbar.LetterboxDetails @@ -148,4 +150,20 @@ private data class SystemBarAttributesParams( ) { val letterboxesArray = letterboxes.toTypedArray() val appearanceRegionsArray = appearanceRegions.toTypedArray() + override fun toString(): String { + val appearanceToString = + ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) + return """SystemBarAttributesParams( + displayId=$displayId, + appearance=$appearanceToString, + appearanceRegions=$appearanceRegions, + navbarColorManagedByIme=$navbarColorManagedByIme, + behavior=$behavior, + requestedVisibilities=$requestedVisibilities, + packageName='$packageName', + letterboxes=$letterboxes, + letterboxesArray=${letterboxesArray.contentToString()}, + appearanceRegionsArray=${appearanceRegionsArray.contentToString()} + )""".trimMargin() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index 59603874efde..5562e73f0478 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -38,4 +40,12 @@ sealed interface ResolvedNetworkType { data class OverrideNetworkType( override val lookupKey: String, ) : ResolvedNetworkType + + /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */ + object CarrierMergedNetworkType : ResolvedNetworkType { + // Effectively unused since [iconGroupOverride] is used instead. + override val lookupKey: String = "cwf" + + val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index d04996b4d6ce..6187f64e011d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -22,7 +22,6 @@ import android.telephony.TelephonyManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -50,7 +49,7 @@ interface MobileConnectionRepository { * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single * listener + model. */ - val connectionInfo: Flow<MobileConnectionModel> + val connectionInfo: StateFlow<MobileConnectionModel> /** The total number of levels. Used with [SignalDrawable]. */ val numberOfLevels: StateFlow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 97b4c2cadbe5..e0d156aa25f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.provider.Settings import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config @@ -37,6 +38,15 @@ interface MobileConnectionsRepository { /** Observable for the subscriptionId of the current mobile data connection */ val activeMobileDataSubscriptionId: StateFlow<Int> + /** + * Observable event for when the active data sim switches but the group stays the same. E.g., + * CBRS switching would trigger this + */ + val activeSubChangedInGroupEvent: Flow<Unit> + + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ + val defaultDataSubId: StateFlow<Int> + /** The current connectivity status for the default mobile network connection */ val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index 0c8593d60cf5..b93985604fb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -124,6 +124,9 @@ constructor( realRepository.activeMobileDataSubscriptionId.value ) + override val activeSubChangedInGroupEvent: Flow<Unit> = + activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent } + override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> = activeRepo .flatMapLatest { it.defaultDataSubRatConfig } @@ -139,6 +142,11 @@ constructor( override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> = activeRepo.flatMapLatest { it.defaultMobileIconGroup } + override val defaultDataSubId: StateFlow<Int> = + activeRepo + .flatMapLatest { it.defaultDataSubId } + .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = activeRepo .flatMapLatest { it.defaultMobileNetworkConnectivity } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 0e164e7ee859..108834521ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -39,11 +39,16 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -60,15 +65,19 @@ import kotlinx.coroutines.launch class DemoMobileConnectionsRepository @Inject constructor( - private val dataSource: DemoModeMobileConnectionDataSource, + private val mobileDataSource: DemoModeMobileConnectionDataSource, + private val wifiDataSource: DemoModeWifiDataSource, @Application private val scope: CoroutineScope, context: Context, private val logFactory: TableLogBufferFactory, ) : MobileConnectionsRepository { - private var demoCommandJob: Job? = null + private var mobileDemoCommandJob: Job? = null + private var wifiDemoCommandJob: Job? = null - private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>() + private var carrierMergedSubId: Int? = null + + private var connectionRepoCache = mutableMapOf<Int, CacheContainer>() private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>() val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) @@ -112,6 +121,9 @@ constructor( subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID ) + // TODO(b/261029387): consider adding a demo command for this + override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf() + /** Demo mode doesn't currently support modifications to the mobile mappings */ override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config.readConfig(context)) @@ -140,56 +152,94 @@ constructor( private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key } + // TODO(b/261029387): add a command for this value + override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + // TODO(b/261029387): not yet supported - override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) + override val defaultMobileNetworkConnectivity = + MutableStateFlow(MobileConnectivityModel(isConnected = true, isValidated = true)) override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { - return connectionRepoCache[subId] - ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it } + val current = connectionRepoCache[subId]?.repo + if (current != null) { + return current + } + + val new = createDemoMobileConnectionRepo(subId) + connectionRepoCache[subId] = new + return new.repo } - private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { - val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { + val tableLogBuffer = + logFactory.getOrCreate( + "DemoMobileConnectionLog [$subId]", + MOBILE_CONNECTION_BUFFER_SIZE, + ) - return DemoMobileConnectionRepository( - subId, - tableLogBuffer, - ) + val repo = + DemoMobileConnectionRepository( + subId, + tableLogBuffer, + ) + return CacheContainer(repo, lastMobileState = null) } override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) fun startProcessingCommands() { - demoCommandJob = + mobileDemoCommandJob = scope.launch { - dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) } + mobileDataSource.mobileEvents.filterNotNull().collect { event -> + processMobileEvent(event) + } + } + wifiDemoCommandJob = + scope.launch { + wifiDataSource.wifiEvents.filterNotNull().collect { event -> + processWifiEvent(event) + } } } fun stopProcessingCommands() { - demoCommandJob?.cancel() + mobileDemoCommandJob?.cancel() + wifiDemoCommandJob?.cancel() _subscriptions.value = listOf() connectionRepoCache.clear() subscriptionInfoCache.clear() } - private fun processEvent(event: FakeNetworkEventModel) { + private fun processMobileEvent(event: FakeNetworkEventModel) { when (event) { is Mobile -> { processEnabledMobileState(event) } is MobileDisabled -> { - processDisabledMobileState(event) + maybeRemoveSubscription(event.subId) } } } + private fun processWifiEvent(event: FakeWifiEventModel) { + when (event) { + is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged() + is FakeWifiEventModel.Wifi -> disableCarrierMerged() + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) + } + } + private fun processEnabledMobileState(state: Mobile) { // get or create the connection repo, and set its values val subId = state.subId ?: DEFAULT_SUB_ID maybeCreateSubscription(subId) val connection = getRepoForSubId(subId) + connectionRepoCache[subId]?.lastMobileState = state + + // TODO(b/261029387): until we have a command, use the most recent subId + defaultDataSubId.value = subId + // This is always true here, because we split out disabled states at the data-source level connection.dataEnabled.value = true connection.networkName.value = NetworkNameModel.Derived(state.name) @@ -198,14 +248,36 @@ constructor( connection.connectionInfo.value = state.toMobileConnectionModel() } - private fun processDisabledMobileState(state: MobileDisabled) { + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + // The new carrier merged connection is for a different sub ID, so disable carrier merged + // for the current (now old) sub + if (carrierMergedSubId != event.subscriptionId) { + disableCarrierMerged() + } + + // get or create the connection repo, and set its values + val subId = event.subscriptionId + maybeCreateSubscription(subId) + carrierMergedSubId = subId + + val connection = getRepoForSubId(subId) + // This is always true here, because we split out disabled states at the data-source level + connection.dataEnabled.value = true + connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME) + connection.numberOfLevels.value = event.numberOfLevels + connection.cdmaRoaming.value = false + connection.connectionInfo.value = event.toMobileConnectionModel() + Log.e("CCS", "output connection info = ${connection.connectionInfo.value}") + } + + private fun maybeRemoveSubscription(subId: Int?) { if (_subscriptions.value.isEmpty()) { // Nothing to do here return } - val subId = - state.subId + val finalSubId = + subId ?: run { // For sake of usability, we can allow for no subId arg if there is only one // subscription @@ -223,7 +295,21 @@ constructor( _subscriptions.value[0].subscriptionId } - removeSubscription(subId) + removeSubscription(finalSubId) + } + + private fun disableCarrierMerged() { + val currentCarrierMergedSubId = carrierMergedSubId ?: return + + // If this sub ID was previously not carrier merged, we should reset it to its previous + // connection. + val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState + if (lastMobileState != null) { + processEnabledMobileState(lastMobileState) + } else { + // Otherwise, just remove the subscription entirely + removeSubscription(currentCarrierMergedSubId) + } } private fun removeSubscription(subId: Int) { @@ -251,6 +337,10 @@ constructor( ) } + private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel { + return createCarrierMergedConnectionModel(this.level) + } + private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" return DefaultNetworkType(key) @@ -260,9 +350,17 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 + + private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } +class CacheContainer( + var repo: DemoMobileConnectionRepository, + /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ + var lastMobileState: Mobile?, +) + class DemoMobileConnectionRepository( override val subId: Int, override val tableLogBuffer: TableLogBuffer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt new file mode 100644 index 000000000000..c783b12e0c0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -0,0 +1,181 @@ +/* + * 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.mobile.data.repository.prod + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is + * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually + * displayed as a mobile network triangle. + * + * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. + * + * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile + * connection. + */ +class CarrierMergedConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + @Application private val scope: CoroutineScope, + val wifiRepository: WifiRepository, +) : MobileConnectionRepository { + + /** + * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged + * network. + */ + private val network: Flow<WifiNetworkModel.CarrierMerged?> = + combine( + wifiRepository.isWifiEnabled, + wifiRepository.isWifiDefault, + wifiRepository.wifiNetwork, + ) { isEnabled, isDefault, network -> + when { + !isEnabled -> null + !isDefault -> null + network !is WifiNetworkModel.CarrierMerged -> null + network.subscriptionId != subId -> { + Log.w( + TAG, + "Connection repo subId=$subId " + + "does not equal wifi repo subId=${network.subscriptionId}; " + + "not showing carrier merged" + ) + null + } + else -> network + } + } + + override val connectionInfo: StateFlow<MobileConnectionModel> = + network + .map { it.toMobileConnectionModel() } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel()) + + // TODO(b/238425913): Add logging to this class. + // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate. + + // Carrier merged is never roaming. + override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + // TODO(b/238425913): Fetch the carrier merged network name. + override val networkName: StateFlow<NetworkNameModel> = + flowOf(defaultNetworkName) + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + + override val numberOfLevels: StateFlow<Int> = + wifiRepository.wifiNetwork + .map { + if (it is WifiNetworkModel.CarrierMerged) { + it.numberOfLevels + } else { + DEFAULT_NUM_LEVELS + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) + + override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + + private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel { + if (this == null) { + return MobileConnectionModel() + } + + return createCarrierMergedConnectionModel(level) + } + + companion object { + /** + * Creates an instance of [MobileConnectionModel] that represents a carrier merged network + * with the given [level]. + */ + fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel { + return MobileConnectionModel( + primaryLevel = level, + cdmaLevel = level, + // A [WifiNetworkModel.CarrierMerged] instance is always connected. + // (A [WifiNetworkModel.Inactive] represents a disconnected network.) + dataConnectionState = DataConnectionState.Connected, + // TODO(b/238425913): This should come from [WifiRepository.wifiActivity]. + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + // Carrier merged is never roaming + isRoaming = false, + + // TODO(b/238425913): Verify that these fields never change for carrier merged. + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + } + } + + @SysUISingleton + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val wifiRepository: WifiRepository, + ) { + fun build( + subId: Int, + mobileLogger: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + ): MobileConnectionRepository { + return CarrierMergedConnectionRepository( + subId, + mobileLogger, + defaultNetworkName, + scope, + wifiRepository, + ) + } + } +} + +private const val TAG = "CarrierMergedConnectionRepository" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt new file mode 100644 index 000000000000..0f30ae249c31 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -0,0 +1,179 @@ +/* + * 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.mobile.data.repository.prod + +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * A repository that fully implements a mobile connection. + * + * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl] + * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository + * switches between the two types of connections based on whether the connection is currently + * carrier merged (see [setIsCarrierMerged]). + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class FullMobileConnectionRepository( + override val subId: Int, + startingIsCarrierMerged: Boolean, + override val tableLogBuffer: TableLogBuffer, + private val defaultNetworkName: NetworkNameModel, + private val networkNameSeparator: String, + private val globalMobileDataSettingChangedEvent: Flow<Unit>, + @Application scope: CoroutineScope, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, +) : MobileConnectionRepository { + /** + * Sets whether this connection is a typical mobile connection or a carrier merged connection. + */ + fun setIsCarrierMerged(isCarrierMerged: Boolean) { + _isCarrierMerged.value = isCarrierMerged + } + + /** + * Returns true if this repo is currently for a carrier merged connection and false otherwise. + */ + @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value + + private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged) + private val isCarrierMerged: StateFlow<Boolean> = + _isCarrierMerged + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = "isCarrierMerged", + initialValue = startingIsCarrierMerged, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged) + + private val mobileRepo: MobileConnectionRepository by lazy { + mobileRepoFactory.build( + subId, + tableLogBuffer, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + ) + } + + private val carrierMergedRepo: MobileConnectionRepository by lazy { + carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName) + } + + @VisibleForTesting + internal val activeRepo: StateFlow<MobileConnectionRepository> = run { + val initial = + if (startingIsCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + + this.isCarrierMerged + .mapLatest { isCarrierMerged -> + if (isCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } + + override val cdmaRoaming = + activeRepo + .flatMapLatest { it.cdmaRoaming } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value) + + override val connectionInfo = + activeRepo + .flatMapLatest { it.connectionInfo } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value) + + override val dataEnabled = + activeRepo + .flatMapLatest { it.dataEnabled } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value) + + override val numberOfLevels = + activeRepo + .flatMapLatest { it.numberOfLevels } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value) + + override val networkName = + activeRepo + .flatMapLatest { it.networkName } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) + + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val logFactory: TableLogBufferFactory, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, + ) { + fun build( + subId: Int, + startingIsCarrierMerged: Boolean, + defaultNetworkName: NetworkNameModel, + networkNameSeparator: String, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): FullMobileConnectionRepository { + val mobileLogger = + logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE) + + return FullMobileConnectionRepository( + subId, + startingIsCarrierMerged, + mobileLogger, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + scope, + mobileRepoFactory, + carrierMergedRepoFactory, + ) + } + + companion object { + /** The buffer size to use for logging. */ + const val MOBILE_CONNECTION_BUFFER_SIZE = 100 + + /** Returns a log buffer name for a mobile connection with the given [subId]. */ + fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 0fa0fea0bebf..3f2ce4000ff1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -38,7 +38,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -70,6 +69,10 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +/** + * A repository implementation for a typical mobile connection (as opposed to a carrier merged + * connection -- see [CarrierMergedConnectionRepository]). + */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( @@ -298,18 +301,16 @@ class MobileConnectionRepositoryImpl( private val logger: ConnectivityPipelineLogger, private val globalSettings: GlobalSettings, private val mobileMappingsProxy: MobileMappingsProxy, - private val logFactory: TableLogBufferFactory, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { fun build( subId: Int, + mobileLogger: TableLogBuffer, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { - val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) - return MobileConnectionRepositoryImpl( context, subId, @@ -327,8 +328,4 @@ class MobileConnectionRepositoryImpl( ) } } - - companion object { - fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index c88c70064238..510482dcb21e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -35,6 +35,7 @@ import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.annotation.VisibleForTesting +import com.android.internal.telephony.PhoneConstants import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings.Config import com.android.systemui.R @@ -46,11 +47,13 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.util.kotlin.pairwiseBy import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -59,9 +62,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge @@ -85,9 +91,14 @@ constructor( private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, - private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory + // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi + // repository is an input to the mobile repository. + // See [CarrierMergedConnectionRepository] for details. + wifiRepository: WifiRepository, + private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, ) : MobileConnectionsRepository { - private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> = + mutableMapOf() private val defaultNetworkName = NetworkNameModel.Default( @@ -97,30 +108,43 @@ constructor( private val networkNameSeparator: String = context.getString(R.string.status_bar_network_name_separator) + private val carrierMergedSubId: StateFlow<Int?> = + wifiRepository.wifiNetwork + .mapLatest { + if (it is WifiNetworkModel.CarrierMerged) { + it.subscriptionId + } else { + null + } + } + .distinctUntilChanged() + .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) + + private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + /** * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. + * [SubscriptionModel]. */ override val subscriptions: StateFlow<List<SubscriptionModel>> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } + merge(mobileSubscriptionsChangeEvent, carrierMergedSubId) .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } .logInputChange(logger, "onSubscriptionsChanged") - .onEach { infos -> dropUnusedReposFromCache(infos) } + .onEach { infos -> updateRepos(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) /** StateFlow that keeps track of the current active mobile data subscription */ @@ -140,10 +164,24 @@ constructor( .logInputChange(logger, "onActiveDataSubscriptionIdChanged") .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) - private val defaultDataSubIdChangedEvent = + private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow<Int> = broadcastDispatcher - .broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) + .broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) { intent, _ -> + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + } + .distinctUntilChanged() .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED") + .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + SubscriptionManager.getDefaultDataSubscriptionId() + ) private val carrierConfigChangedEvent = broadcastDispatcher @@ -151,7 +189,7 @@ constructor( .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED") override val defaultDataSubRatConfig: StateFlow<Config> = - merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent) + merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) .mapLatest { Config.readConfig(context) } .distinctUntilChanged() .logInputChange(logger, "defaultDataSubRatConfig") @@ -173,7 +211,7 @@ constructor( .distinctUntilChanged() .logInputChange(logger, "defaultMobileIconGroup") - override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository { if (!isValidSubId(subId)) { throw IllegalArgumentException( "subscriptionId $subId is not in the list of valid subscriptions" @@ -239,6 +277,35 @@ constructor( .logInputChange(logger, "defaultMobileNetworkConnectivity") .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) + /** + * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data + * subscription Id changes but the subscription group remains the same. In these cases, we want + * to retain the previous subscription's validation status for up to 2s to avoid flickering the + * icon. + * + * TODO(b/265164432): we should probably expose all change events, not just same group + */ + @SuppressLint("MissingPermission") + override val activeSubChangedInGroupEvent = + flow { + activeMobileDataSubscriptionId.pairwiseBy { prevVal: Int, newVal: Int -> + if (!defaultMobileNetworkConnectivity.value.isValidated) { + return@pairwiseBy + } + val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal) + val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal) + + if (prevSub == null || nextSub == null) { + return@pairwiseBy + } + + if (prevSub.groupUuid != null && prevSub.groupUuid == nextSub.groupUuid) { + emit(Unit) + } + } + } + .flowOn(bgDispatcher) + private fun isValidSubId(subId: Int): Boolean { subscriptions.value.forEach { if (it.subscriptionId == subId) { @@ -251,15 +318,27 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache - private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build( + private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { + return fullMobileRepoFactory.build( subId, + isCarrierMerged(subId), defaultNetworkName, networkNameSeparator, globalMobileDataSettingChangedEvent, ) } + private fun updateRepos(newInfos: List<SubscriptionModel>) { + dropUnusedReposFromCache(newInfos) + subIdRepositoryCache.forEach { (subId, repo) -> + repo.setIsCarrierMerged(isCarrierMerged(subId)) + } + } + + private fun isCarrierMerged(subId: Int): Boolean { + return subId == carrierMergedSubId.value + } + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) { // Remove any connection repository from the cache that isn't in the new set of IDs. They // will get garbage collected once their subscribers go away diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 9427c6b9fece..9cdff96dc7d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -18,12 +18,14 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons.NOT_DEFAULT_DATA import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -41,12 +43,29 @@ interface MobileIconInteractor { /** The current mobile data activity */ val activity: Flow<DataActivityModel> - /** Only true if mobile is the default transport but is not validated, otherwise false */ - val isDefaultConnectionFailed: StateFlow<Boolean> + /** + * This bit is meant to be `true` if and only if the default network capabilities (see + * [android.net.ConnectivityManager.registerDefaultNetworkCallback]) result in a network that + * has the [android.net.NetworkCapabilities.TRANSPORT_CELLULAR] represented. + * + * Note that this differs from [isDataConnected], which is tracked by telephony and has to do + * with the state of using this mobile connection for data as opposed to just voice. It is + * possible for a mobile subscription to be connected but not be in a connected data state, and + * thus we wouldn't want to show the network type icon. + */ + val isConnected: Flow<Boolean> - /** True when telephony tells us that the data state is CONNECTED */ + /** + * True when telephony tells us that the data state is CONNECTED. See + * [android.telephony.TelephonyCallback.DataConnectionStateListener] for more details. We + * consider this connection to be serving data, and thus want to show a network type icon, when + * data is connected. Other data connection states would typically cause us not to show the icon + */ val isDataConnected: StateFlow<Boolean> + /** Only true if mobile is the default transport but is not validated, otherwise false */ + val isDefaultConnectionFailed: StateFlow<Boolean> + /** True if we consider this connection to be in service, i.e. can make calls */ val isInService: StateFlow<Boolean> @@ -100,8 +119,10 @@ class MobileIconInteractorImpl( defaultSubscriptionHasDataEnabled: StateFlow<Boolean>, override val alwaysShowDataRatIcon: StateFlow<Boolean>, override val alwaysUseCdmaLevel: StateFlow<Boolean>, + defaultMobileConnectivity: StateFlow<MobileConnectivityModel>, defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>, defaultMobileIconGroup: StateFlow<MobileIconGroup>, + defaultDataSubId: StateFlow<Int>, override val isDefaultConnectionFailed: StateFlow<Boolean>, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { @@ -111,8 +132,19 @@ class MobileIconInteractorImpl( override val activity = connectionInfo.mapLatest { it.dataActivityDirection } + override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected } + override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled + private val isDefault = + defaultDataSubId + .mapLatest { connectionRepository.subId == it } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + connectionRepository.subId == defaultDataSubId.value + ) + override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled override val networkName = @@ -137,8 +169,17 @@ class MobileIconInteractorImpl( connectionInfo, defaultMobileIconMapping, defaultMobileIconGroup, - ) { info, mapping, defaultGroup -> - mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + isDefault, + ) { info, mapping, defaultGroup, isDefault -> + if (!isDefault) { + return@combine NOT_DEFAULT_DATA + } + + when (info.resolvedNetworkType) { + is ResolvedNetworkType.CarrierMergedNetworkType -> + info.resolvedNetworkType.iconGroupOverride + else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + } } .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 83da1dd06780..9ae38e973bd0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -23,6 +23,7 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository @@ -31,14 +32,17 @@ import com.android.systemui.util.CarrierConfigTracker import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest /** * Business layer logic for the set of mobile subscription icons. @@ -62,6 +66,17 @@ interface MobileIconsInteractor { /** True if the CDMA level should be preferred over the primary level. */ val alwaysUseCdmaLevel: StateFlow<Boolean> + /** Tracks the subscriptionId set as the default for data connections */ + val defaultDataSubId: StateFlow<Int> + + /** + * The connectivity of the default mobile network. Note that this can differ from what is + * reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription + * changes but the groupUuid remains the same, we keep the old validation information for 2 + * seconds to avoid icon flickering. + */ + val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> + /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ @@ -154,6 +169,48 @@ constructor( } } + override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId + + /** + * Copied from the old pipeline. We maintain a 2s period of time where we will keep the + * validated bit from the old active network (A) while data is changing to the new one (B). + * + * This condition only applies if + * 1. A and B are in the same subscription group (e.c. for CBRS data switching) and + * 2. A was validated before the switch + * + * The goal of this is to minimize the flickering in the UI of the cellular indicator + */ + private val forcingCellularValidation = + mobileConnectionsRepo.activeSubChangedInGroupEvent + .filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated } + .transformLatest { + emit(true) + delay(2000) + emit(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = + combine( + mobileConnectionsRepo.defaultMobileNetworkConnectivity, + forcingCellularValidation, + ) { networkConnectivity, forceValidation -> + return@combine if (forceValidation) { + MobileConnectivityModel( + isValidated = true, + isConnected = networkConnectivity.isConnected + ) + } else { + networkConnectivity + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + mobileConnectionsRepo.defaultMobileNetworkConnectivity.value + ) + /** * Mapping from network type to [MobileIconGroup] using the config generated for the default * subscription Id. This mapping is the same for every subscription. @@ -207,8 +264,10 @@ constructor( activeDataConnectionHasDataEnabled, alwaysShowDataRatIcon, alwaysUseCdmaLevel, + defaultMobileNetworkConnectivity, defaultMobileIconMapping, defaultMobileIconGroup, + defaultDataSubId, isDefaultConnectionFailed, mobileConnectionsRepo.getRepoForSubId(subId), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index a2117c7df188..5e9356163e6e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -102,24 +102,29 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), initial) } + private val showNetworkTypeIcon: Flow<Boolean> = + combine( + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed, + iconInteractor.alwaysShowDataRatIcon, + iconInteractor.isConnected, + ) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected -> + alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected) + } + override val networkTypeIcon: Flow<Icon?> = combine( iconInteractor.networkTypeIconGroup, - iconInteractor.isDataConnected, - iconInteractor.isDataEnabled, - iconInteractor.isDefaultConnectionFailed, - iconInteractor.alwaysShowDataRatIcon, - ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow -> + showNetworkTypeIcon, + ) { networkTypeIconGroup, shouldShow -> val desc = if (networkTypeIconGroup.dataContentDescription != 0) ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) else null val icon = Icon.Resource(networkTypeIconGroup.dataType, desc) return@combine when { - alwaysShow -> icon - !dataConnected -> null - !dataEnabled -> null - failedConnection -> null + !shouldShow -> null else -> icon } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt index 4251d18357f7..da2daf2c55ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.annotation.VisibleForTesting import com.android.systemui.log.table.TableRowLogger import com.android.systemui.log.table.Diffable +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS /** Provides information about the current wifi network. */ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { + // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of + // copy-pasting the column names for each sub-object. + /** * A model representing that we couldn't fetch any wifi information. * @@ -41,8 +46,43 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { override fun logFull(row: TableRowLogger) { row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE) row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) + } + } + + /** + * A model representing that the wifi information we received was invalid in some way. + */ + data class Invalid( + /** A description of why the wifi information was invalid. */ + val invalidReason: String, + ) : WifiNetworkModel() { + override fun toString() = "WifiNetwork.Invalid[$invalidReason]" + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { + if (prevVal !is Invalid) { + logFull(row) + return + } + + if (invalidReason != prevVal.invalidReason) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) row.logChange(COL_VALIDATED, false) row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) row.logChange(COL_SSID, null) row.logChange(COL_PASSPOINT_ACCESS_POINT, false) row.logChange(COL_ONLINE_SIGN_UP, false) @@ -59,18 +99,21 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { return } - if (prevVal is CarrierMerged) { - // The only difference between CarrierMerged and Inactive is the type - row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) - return - } - - // When changing from Active to Inactive, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_INACTIVE, row) + // When changing to Inactive, we need to log diffs to all the fields. + logFull(row) } override fun logFull(row: TableRowLogger) { - logFullNonActiveNetwork(TYPE_INACTIVE, row) + row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -80,22 +123,75 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { * * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. */ - object CarrierMerged : WifiNetworkModel() { - override fun toString() = "WifiNetwork.CarrierMerged" + data class CarrierMerged( + /** + * The [android.net.Network.netId] we received from + * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network. + * + * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId]. + */ + val networkId: Int, + + /** + * The subscription ID that this connection represents. + * + * Comes from [android.net.wifi.WifiInfo.getSubscriptionId]. + * + * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid, + * then this is *not* a carrier merged network). + */ + val subscriptionId: Int, + + /** + * The signal level, guaranteed to be 0 <= level <= numberOfLevels. + */ + val level: Int, + + /** + * The maximum possible level. + */ + val numberOfLevels: Int = DEFAULT_NUM_LEVELS, + ) : WifiNetworkModel() { + init { + require(level in MIN_VALID_LEVEL..numberOfLevels) { + "0 <= wifi level <= $numberOfLevels required; level was $level" + } + require(subscriptionId != INVALID_SUBSCRIPTION_ID) { + "subscription ID cannot be invalid" + } + } override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { - if (prevVal is CarrierMerged) { + if (prevVal !is CarrierMerged) { + logFull(row) return } - if (prevVal is Inactive) { - // The only difference between CarrierMerged and Inactive is the type. - row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) - return + if (prevVal.networkId != networkId) { + row.logChange(COL_NETWORK_ID, networkId) } + if (prevVal.subscriptionId != subscriptionId) { + row.logChange(COL_SUB_ID, subscriptionId) + } + if (prevVal.level != level) { + row.logChange(COL_LEVEL, level) + } + if (prevVal.numberOfLevels != numberOfLevels) { + row.logChange(COL_NUM_LEVELS, numberOfLevels) + } + } - // When changing from Active to CarrierMerged, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row) + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, subscriptionId) + row.logChange(COL_VALIDATED, true) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, numberOfLevels) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -137,38 +233,50 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { if (prevVal !is Active) { - row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + logFull(row) + return } - if (prevVal !is Active || prevVal.networkId != networkId) { + if (prevVal.networkId != networkId) { row.logChange(COL_NETWORK_ID, networkId) } - if (prevVal !is Active || prevVal.isValidated != isValidated) { + if (prevVal.isValidated != isValidated) { row.logChange(COL_VALIDATED, isValidated) } - if (prevVal !is Active || prevVal.level != level) { + if (prevVal.level != level) { row.logChange(COL_LEVEL, level) } - if (prevVal !is Active || prevVal.ssid != ssid) { + if (prevVal.ssid != ssid) { row.logChange(COL_SSID, ssid) } // TODO(b/238425913): The passpoint-related values are frequently never used, so it // would be great to not log them when they're not used. - if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { + if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.isOnlineSignUpForPasspointAccessPoint != + if (prevVal.isOnlineSignUpForPasspointAccessPoint != isOnlineSignUpForPasspointAccessPoint) { row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { + if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) } } + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, null) + row.logChange(COL_VALIDATED, isValidated) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, null) + row.logChange(COL_SSID, ssid) + row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) + row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) + row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) + } + override fun toString(): String { // Only include the passpoint-related values in the string if we have them. (Most // networks won't have them so they'll be mostly clutter.) @@ -189,21 +297,13 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { companion object { @VisibleForTesting - internal const val MIN_VALID_LEVEL = 0 - @VisibleForTesting internal const val MAX_VALID_LEVEL = 4 } } - internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) { - row.logChange(COL_NETWORK_TYPE, type) - row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) - row.logChange(COL_VALIDATED, false) - row.logChange(COL_LEVEL, LEVEL_DEFAULT) - row.logChange(COL_SSID, null) - row.logChange(COL_PASSPOINT_ACCESS_POINT, false) - row.logChange(COL_ONLINE_SIGN_UP, false) - row.logChange(COL_PASSPOINT_NAME, null) + companion object { + @VisibleForTesting + internal const val MIN_VALID_LEVEL = 0 } } @@ -214,12 +314,16 @@ const val TYPE_ACTIVE = "Active" const val COL_NETWORK_TYPE = "type" const val COL_NETWORK_ID = "networkId" +const val COL_SUB_ID = "subscriptionId" const val COL_VALIDATED = "isValidated" const val COL_LEVEL = "level" +const val COL_NUM_LEVELS = "maxLevel" const val COL_SSID = "ssid" const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint" const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint" const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName" val LEVEL_DEFAULT: String? = null +val NUM_LEVELS_DEFAULT: String? = null val NETWORK_ID_DEFAULT: String? = null +val SUB_ID_DEFAULT: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt index c588945fbd67..caac8fa2f2c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,10 +44,10 @@ constructor( private fun Bundle.toWifiEvent(): FakeWifiEventModel? { val wifi = getString("wifi") ?: return null - return if (wifi == "show") { - activeWifiEvent() - } else { - FakeWifiEventModel.WifiDisabled + return when (wifi) { + "show" -> activeWifiEvent() + "carriermerged" -> carrierMergedWifiEvent() + else -> FakeWifiEventModel.WifiDisabled } } @@ -64,6 +65,14 @@ constructor( ) } + private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged { + val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID + val level = getString("level")?.toInt() ?: 0 + val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS + + return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels) + } + private fun String.toActivity(): Int = when (this) { "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT @@ -71,4 +80,8 @@ constructor( "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE } + + companion object { + const val DEFAULT_CARRIER_MERGED_SUB_ID = 10 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt index be3d7d4e65c4..e161b3e42d02 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -66,6 +66,7 @@ constructor( private fun processEvent(event: FakeWifiEventModel) = when (event) { is FakeWifiEventModel.Wifi -> processEnabledWifiState(event) + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState() } @@ -85,6 +86,14 @@ constructor( _wifiNetwork.value = event.toWifiNetworkModel() } + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + _isWifiEnabled.value = true + _isWifiDefault.value = true + // TODO(b/238425913): Support activity in demo mode. + _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = event.toCarrierMergedModel() + } + private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel = WifiNetworkModel.Active( networkId = DEMO_NET_ID, @@ -99,6 +108,14 @@ constructor( passpointProviderFriendlyName = null, ) + private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel = + WifiNetworkModel.CarrierMerged( + networkId = DEMO_NET_ID, + subscriptionId = subscriptionId, + level = level, + numberOfLevels = numberOfLevels, + ) + companion object { private const val DEMO_NET_ID = 1234 } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt index 2353fb82f3b1..518f8ce66d2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt @@ -29,5 +29,11 @@ sealed interface FakeWifiEventModel { val validated: Boolean?, ) : FakeWifiEventModel + data class CarrierMerged( + val subscriptionId: Int, + val level: Int, + val numberOfLevels: Int, + ) : FakeWifiEventModel + object WifiDisabled : FakeWifiEventModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index c47c20d280c7..d26499c18661 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -29,6 +29,7 @@ import android.net.NetworkRequest import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -269,7 +270,19 @@ constructor( wifiManager: WifiManager, ): WifiNetworkModel { return if (wifiInfo.isCarrierMerged) { - WifiNetworkModel.CarrierMerged + if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) { + WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON) + } else { + WifiNetworkModel.CarrierMerged( + networkId = network.getNetId(), + subscriptionId = wifiInfo.subscriptionId, + level = wifiManager.calculateSignalLevel(wifiInfo.rssi), + // The WiFi signal level returned by WifiManager#calculateSignalLevel start + // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level + // buckets count. + numberOfLevels = wifiManager.maxSignalLevel + 1, + ) + } } else { WifiNetworkModel.Active( network.getNetId(), @@ -302,6 +315,9 @@ constructor( .build() private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel" + + private const val CARRIER_MERGED_INVALID_SUB_ID_REASON = + "Wifi network was carrier merged but had invalid sub ID" } @SysUISingleton 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 980560ab5d58..86dcd18c643c 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 @@ -66,6 +66,7 @@ class WifiInteractorImpl @Inject constructor( override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> when (info) { is WifiNetworkModel.Unavailable -> null + is WifiNetworkModel.Invalid -> null is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Active -> when { 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 824b5972ba4b..95431afb71bb 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 @@ -83,6 +83,7 @@ constructor( private fun WifiNetworkModel.icon(): WifiIcon { return when (this) { is WifiNetworkModel.Unavailable -> WifiIcon.Hidden + is WifiNetworkModel.Invalid -> WifiIcon.Hidden is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden is WifiNetworkModel.Inactive -> WifiIcon.Visible( res = WIFI_NO_NETWORK, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index 68d30d3f3d1e..2b4f51c63043 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -60,7 +60,7 @@ protected constructor( * animation to and from the parent dialog. */ @JvmOverloads - open fun onUserListItemClicked( + fun onUserListItemClicked( record: UserRecord, dialogShower: DialogShower? = null, ) { 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 f63d65246d9b..c8ee647cf8a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -160,7 +160,7 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mStatusBarStateController = statusBarStateController; mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, dozeParameters, - screenOffAnimationController, /* animateYPos= */ false); + screenOffAnimationController, /* animateYPos= */ false, /* logBuffer= */ null); mUserSwitchDialogController = userSwitchDialogController; mUiEventLogger = uiEventLogger; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java index c1506541229d..e9f0dcb4eb51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java @@ -173,7 +173,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS mUserSwitcherController, this); mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, dozeParameters, - screenOffAnimationController, /* animateYPos= */ false); + screenOffAnimationController, /* animateYPos= */ false, /* logBuffer= */ null); mBackground = new KeyguardUserSwitcherScrim(context); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index c9ed0cb4155d..f8c17e8c8379 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -109,6 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; + private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; public final Object mToken = new Object(); @@ -421,7 +423,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @VisibleForTesting - void onDefocus(boolean animate, boolean logClose) { + void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); @@ -431,18 +433,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ViewGroup parent = (ViewGroup) getParent(); if (animate && parent != null && mIsFocusAnimationFlagActive) { - ViewGroup grandParent = (ViewGroup) parent.getParent(); ViewGroupOverlay overlay = parent.getOverlay(); + View actionsContainer = getActionsContainerLayout(); + int actionsContainerHeight = + actionsContainer != null ? actionsContainer.getHeight() : 0; // After adding this RemoteInputView to the overlay of the parent (and thus removing // it from the parent itself), the parent will shrink in height. This causes the // overlay to be moved. To correct the position of the overlay we need to offset it. - int overlayOffsetY = getMaxSiblingHeight() - getHeight(); + int overlayOffsetY = actionsContainerHeight - getHeight(); overlay.add(this); if (grandParent != null) grandParent.setClipChildren(false); - Animator animator = getDefocusAnimator(overlayOffsetY); + Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); View self = this; animator.addListener(new AnimatorListenerAdapter() { @Override @@ -454,8 +458,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } + if (doAfterDefocus != null) { + doAfterDefocus.run(); + } } }); + if (actionsContainer != null) actionsContainer.setAlpha(0f); animator.start(); } else if (animate && mRevealParams != null && mRevealParams.radius > 0) { @@ -474,6 +482,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene reveal.start(); } else { setVisibility(GONE); + if (doAfterDefocus != null) doAfterDefocus.run(); if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } @@ -596,10 +605,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * Focuses the RemoteInputView and animates its appearance - * - * @param crossFadeView view that will be crossfaded during the appearance animation */ - public void focusAnimated(View crossFadeView) { + public void focusAnimated() { if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE && mRevealParams != null) { android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); @@ -609,7 +616,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { mIsAnimatingAppearance = true; setAlpha(0f); - Animator focusAnimator = getFocusAnimator(crossFadeView); + Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); focusAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { @@ -661,6 +668,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void reset() { + if (mIsFocusAnimationFlagActive) { + mProgressBar.setVisibility(INVISIBLE); + mResetting = true; + mSending = false; + onDefocus(true /* animate */, false /* logClose */, () -> { + mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); + mEditText.getText().clear(); + mEditText.setEnabled(isAggregatedVisible()); + mSendButton.setVisibility(VISIBLE); + mController.removeSpinning(mEntry.getKey(), mToken); + updateSendButton(); + setAttachment(null); + mResetting = false; + }); + return; + } + mResetting = true; mSending = false; mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); @@ -671,7 +695,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mProgressBar.setVisibility(INVISIBLE); mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); - onDefocus(false /* animate */, false /* logClose */); + onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); setAttachment(null); mResetting = false; @@ -825,23 +849,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * @return max sibling height (0 in case of no siblings) + * @return action button container view (i.e. ViewGroup containing Reply button etc.) */ - public int getMaxSiblingHeight() { + public View getActionsContainerLayout() { ViewGroup parentView = (ViewGroup) getParent(); - int maxHeight = 0; - if (parentView == null) return 0; - for (int i = 0; i < parentView.getChildCount(); i++) { - View siblingView = parentView.getChildAt(i); - if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight()); - } - return maxHeight; + if (parentView == null) return null; + return parentView.findViewById(com.android.internal.R.id.actions_container_layout); } /** * Creates an animator for the focus animation. + * + * @param fadeOutView View that will be faded out during the focus animation. */ - private Animator getFocusAnimator(View crossFadeView) { + private Animator getFocusAnimator(@Nullable View fadeOutView) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); @@ -854,30 +877,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); - final Animator crossFadeViewAlphaAnimator = - ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f); - crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); - crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); - alphaAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation, boolean isReverse) { - crossFadeView.setAlpha(1f); - } - }); - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator); + if (fadeOutView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + final Animator fadeOutViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); + fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation, boolean isReverse) { + fadeOutView.setAlpha(1f); + } + }); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); + } return animatorSet; } /** * Creates an animator for the defocus animation. * - * @param offsetY The RemoteInputView will be offset by offsetY during the animation + * @param fadeInView View that will be faded in during the defocus animation. + * @param offsetY The RemoteInputView will be offset by offsetY during the animation */ - private Animator getDefocusAnimator(int offsetY) { + private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); - alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); @@ -893,8 +922,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } }); - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator); + if (fadeInView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + fadeInView.forceHasOverlappingRendering(false); + Animator fadeInViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); + fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); + } return animatorSet; } @@ -1011,7 +1049,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (isFocusable() && isEnabled()) { setInnerFocusable(false); if (mRemoteInputView != null) { - mRemoteInputView.onDefocus(animate, true /* logClose */); + mRemoteInputView + .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); } mShowImeOnInputConnection = false; } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt deleted file mode 100644 index 154c6e2e3158..000000000000 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.util.Log -import android.view.InputDevice -import androidx.annotation.VisibleForTesting -import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * A listener that detects when a stylus has first been used, by detecting 1) the presence of an - * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth - * address. - */ -@SysUISingleton -class StylusFirstUsageListener -@Inject -constructor( - private val context: Context, - private val inputManager: InputManager, - private val stylusManager: StylusManager, - private val featureFlags: FeatureFlags, - @Background private val executor: Executor, - @Background private val handler: Handler, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - // Set must be only accessed from the background handler, which is the same handler that - // runs the StylusManager callbacks. - private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf() - @VisibleForTesting var hasStarted = false - - override fun start() { - if (true) return // TODO(b/261826950): remove on main - if (hasStarted) return - if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return - if (inputManager.isStylusEverUsed(context)) return - if (!hostDeviceSupportsStylusInput()) return - - hasStarted = true - inputManager.inputDeviceIds.forEach(this::onStylusAdded) - stylusManager.registerCallback(this) - stylusManager.startListener() - } - - override fun onStylusAdded(deviceId: Int) { - if (!hasStarted) return - - val device = inputManager.getInputDevice(deviceId) ?: return - if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return - - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - internalStylusDeviceIds += deviceId - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.") - } - } - - override fun onStylusRemoved(deviceId: Int) { - if (!hasStarted) return - - if (!internalStylusDeviceIds.contains(deviceId)) return - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - internalStylusDeviceIds.remove(deviceId) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") - } - } - - override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { - if (!hasStarted) return - - onRemoteDeviceFound() - } - - override fun onBatteryStateChanged( - deviceId: Int, - eventTimeMillis: Long, - batteryState: BatteryState - ) { - if (!hasStarted) return - - if (batteryState.isPresent) { - onRemoteDeviceFound() - } - } - - private fun onRemoteDeviceFound() { - inputManager.setStylusEverUsed(context, true) - cleanupListeners() - } - - private fun cleanupListeners() { - stylusManager.unregisterCallback(this) - handler.post { - internalStylusDeviceIds.forEach { - inputManager.removeInputDeviceBatteryListener(it, this) - } - } - } - - private fun hostDeviceSupportsStylusInput(): Boolean { - return inputManager.inputDeviceIds - .asSequence() - .mapNotNull { inputManager.getInputDevice(it) } - .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } - } - - companion object { - private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 302d6a9ca1b7..235495cfa50d 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -18,6 +18,8 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.content.Context +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.util.ArrayMap @@ -25,6 +27,8 @@ import android.util.Log import android.view.InputDevice import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject @@ -37,25 +41,37 @@ import javax.inject.Inject class StylusManager @Inject constructor( + private val context: Context, private val inputManager: InputManager, private val bluetoothAdapter: BluetoothAdapter?, @Background private val handler: Handler, @Background private val executor: Executor, -) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener { + private val featureFlags: FeatureFlags, +) : + InputManager.InputDeviceListener, + InputManager.InputDeviceBatteryListener, + BluetoothAdapter.OnMetadataChangedListener { private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList() private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> = CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() + // This variable should only be accessed on the handler + private var hasStarted: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot * at time of starting. */ fun startListener() { - addExistingStylusToMap() - inputManager.registerInputDeviceListener(this, handler) + handler.post { + if (hasStarted) return@post + hasStarted = true + addExistingStylusToMap() + + inputManager.registerInputDeviceListener(this, handler) + } } /** Registers a StylusCallback to listen to stylus events. */ @@ -77,21 +93,30 @@ constructor( } override fun onInputDeviceAdded(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return + if (!device.isExternal) { + registerBatteryListener(deviceId) + } + // TODO(b/257936830): get address once input api available val btAddress: String? = null inputDeviceAddressMap[deviceId] = btAddress executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) } if (btAddress != null) { + onStylusUsed() onStylusBluetoothConnected(btAddress) executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) } } } override fun onInputDeviceChanged(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return @@ -112,7 +137,10 @@ constructor( } override fun onInputDeviceRemoved(deviceId: Int) { + if (!hasStarted) return + if (!inputDeviceAddressMap.contains(deviceId)) return + unregisterBatteryListener(deviceId) val btAddress: String? = inputDeviceAddressMap[deviceId] inputDeviceAddressMap.remove(deviceId) @@ -124,13 +152,14 @@ constructor( } override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) { - handler.post executeMetadataChanged@{ - if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) - return@executeMetadataChanged + handler.post { + if (!hasStarted) return@post + + if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post val inputDeviceId: Int = inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull() - ?: return@executeMetadataChanged + ?: return@post val isCharging = String(value) == "true" @@ -140,6 +169,24 @@ constructor( } } + override fun onBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState + ) { + handler.post { + if (!hasStarted) return@post + + if (batteryState.isPresent) { + onStylusUsed() + } + + executeStylusBatteryCallbacks { cb -> + cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState) + } + } + } + private fun onStylusBluetoothConnected(btAddress: String) { val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { @@ -158,6 +205,21 @@ constructor( } } + /** + * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a + * physical stylus device has never been used. This method is run when 1) a USI stylus battery + * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a + * physical stylus device has actually been used. + */ + private fun onStylusUsed() { + if (true) return // TODO(b/261826950): remove on main + if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return + if (inputManager.isStylusEverUsed(context)) return + + inputManager.setStylusEverUsed(context, true) + executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } + } + private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } @@ -166,31 +228,69 @@ constructor( stylusBatteryCallbacks.forEach(run) } + private fun registerBatteryListener(deviceId: Int) { + try { + inputManager.addInputDeviceBatteryListener(deviceId, executor, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") + } + } + + private fun unregisterBatteryListener(deviceId: Int) { + // If deviceId wasn't registered, the result is a no-op, so an "is registered" + // check is not needed. + try { + inputManager.removeInputDeviceBatteryListener(deviceId, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") + } + } + private fun addExistingStylusToMap() { for (deviceId: Int in inputManager.inputDeviceIds) { val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue if (device.supportsSource(InputDevice.SOURCE_STYLUS)) { // TODO(b/257936830): get address once input api available inputDeviceAddressMap[deviceId] = null + + if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available + // For most devices, an active (non-bluetooth) stylus is represented by an + // internal InputDevice. This InputDevice will be present in InputManager + // before CoreStartables run, and will not be removed. + // In many cases, it reports the battery level of the stylus. + registerBatteryListener(deviceId) + } } } } - /** Callback interface to receive events from the StylusManager. */ + /** + * Callback interface to receive events from the StylusManager. All callbacks are run on the + * same background handler. + */ interface StylusCallback { fun onStylusAdded(deviceId: Int) {} fun onStylusRemoved(deviceId: Int) {} fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {} fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {} + fun onStylusFirstUsed() {} } - /** Callback interface to receive stylus battery events from the StylusManager. */ + /** + * Callback interface to receive stylus battery events from the StylusManager. All callbacks are + * runs on the same background handler. + */ interface StylusBatteryCallback { fun onStylusBluetoothChargingStateChanged( inputDeviceId: Int, btDevice: BluetoothDevice, isCharging: Boolean ) {} + fun onStylusUsiBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState, + ) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 11233dda165c..5a8850a9f89b 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -18,14 +18,11 @@ package com.android.systemui.stylus import android.hardware.BatteryState import android.hardware.input.InputManager -import android.util.Log import android.view.InputDevice import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import java.util.concurrent.Executor import javax.inject.Inject /** @@ -40,16 +37,7 @@ constructor( private val inputManager: InputManager, private val stylusUsiPowerUi: StylusUsiPowerUI, private val featureFlags: FeatureFlags, - @Background private val executor: Executor, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - override fun onStylusAdded(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - registerBatteryListener(deviceId) - } - } +) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback { override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { stylusUsiPowerUi.refresh() @@ -59,57 +47,30 @@ constructor( stylusUsiPowerUi.refresh() } - override fun onStylusRemoved(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - unregisterBatteryListener(deviceId) - } - } - - override fun onBatteryStateChanged( + override fun onStylusUsiBatteryStateChanged( deviceId: Int, eventTimeMillis: Long, batteryState: BatteryState ) { - if (batteryState.isPresent) { - stylusUsiPowerUi.updateBatteryState(batteryState) - } - } - - private fun registerBatteryListener(deviceId: Int) { - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") - } - } - - private fun unregisterBatteryListener(deviceId: Int) { - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.") + if (batteryState.isPresent && batteryState.capacity > 0f) { + stylusUsiPowerUi.updateBatteryState(deviceId, batteryState) } } override fun start() { if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return - addBatteryListenerForInternalStyluses() + if (!hostDeviceSupportsStylusInput()) return + stylusUsiPowerUi.init() stylusManager.registerCallback(this) stylusManager.startListener() } - private fun addBatteryListenerForInternalStyluses() { - // For most devices, an active stylus is represented by an internal InputDevice. - // This InputDevice will be present in InputManager before CoreStartables run, - // and will not be removed. In many cases, it reports the battery level of the stylus. - inputManager.inputDeviceIds + private fun hostDeviceSupportsStylusInput(): Boolean { + return inputManager.inputDeviceIds .asSequence() .mapNotNull { inputManager.getInputDevice(it) } - .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) } - .forEach { onStylusAdded(it.id) } + .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index 70a5b366263e..8d5e01c5b782 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -18,17 +18,21 @@ package com.android.systemui.stylus import android.Manifest import android.app.PendingIntent +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.hardware.BatteryState import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.os.UserHandle +import android.util.Log import android.view.InputDevice import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -53,6 +57,7 @@ constructor( // These values must only be accessed on the handler. private var batteryCapacity = 1.0f private var suppressed = false + private var inputDeviceId: Int? = null fun init() { val filter = @@ -87,10 +92,12 @@ constructor( } } - fun updateBatteryState(batteryState: BatteryState) { + fun updateBatteryState(deviceId: Int, batteryState: BatteryState) { handler.post updateBattery@{ - if (batteryState.capacity == batteryCapacity) return@updateBattery + if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f) + return@updateBattery + inputDeviceId = deviceId batteryCapacity = batteryState.capacity refresh() } @@ -123,13 +130,13 @@ constructor( .setSmallIcon(R.drawable.ic_power_low) .setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY)) .setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY)) - .setContentTitle(context.getString(R.string.stylus_battery_low)) - .setContentText( + .setContentTitle( context.getString( - R.string.battery_low_percent_format, + R.string.stylus_battery_low_percentage, NumberFormat.getPercentInstance().format(batteryCapacity) ) ) + .setContentText(context.getString(R.string.stylus_battery_low_subtitle)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setLocalOnly(true) .setAutoCancel(true) @@ -150,23 +157,41 @@ constructor( } private fun getPendingBroadcast(action: String): PendingIntent? { - return PendingIntent.getBroadcastAsUser( + return PendingIntent.getBroadcast( context, 0, - Intent(action), + Intent(action).setPackage(context.packageName), PendingIntent.FLAG_IMMUTABLE, - UserHandle.CURRENT ) } - private val receiver: BroadcastReceiver = + @VisibleForTesting + internal val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true) ACTION_CLICKED_LOW_BATTERY -> { updateSuppression(true) - // TODO(b/261584943): open USI device details page + if (inputDeviceId == null) return + + val args = Bundle() + args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!) + try { + context.startActivity( + Intent(ACTION_STYLUS_USI_DETAILS) + .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } catch (e: ActivityNotFoundException) { + // In the rare scenario where the Settings app manifest doesn't contain + // the USI details activity, ignore the intent. + Log.e( + StylusUsiPowerUI::class.java.simpleName, + "Cannot open USI details page." + ) + } } } } @@ -177,9 +202,13 @@ constructor( // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41 private const val LOW_BATTERY_THRESHOLD = 0.16f - private val USI_NOTIFICATION_ID = R.string.stylus_battery_low + private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage - private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" - private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" + @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting + const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS" + @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id" + @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args" } } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt new file mode 100644 index 000000000000..e092f01d19f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.telephony.ui.activity + +import android.app.ActivityOptions +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.UserHandle +import android.util.Log +import android.view.WindowManager +import com.android.internal.app.AlertActivity +import com.android.systemui.R + +/** Dialog shown to the user to switch to managed profile for making a call using work SIM. */ +class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.OnClickListener { + private lateinit var phoneNumber: Uri + private var managedProfileUserId = UserHandle.USER_NULL + + override fun onCreate(savedInstanceState: Bundle?) { + window.addSystemFlags( + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS + ) + super.onCreate(savedInstanceState) + + phoneNumber = intent.getData() + managedProfileUserId = + intent.getIntExtra( + "android.telecom.extra.MANAGED_PROFILE_USER_ID", + UserHandle.USER_NULL + ) + + mAlertParams.apply { + mTitle = getString(R.string.call_from_work_profile_title) + mMessage = getString(R.string.call_from_work_profile_text) + mPositiveButtonText = getString(R.string.call_from_work_profile_action) + mNegativeButtonText = getString(R.string.call_from_work_profile_close) + mPositiveButtonListener = this@SwitchToManagedProfileForCallActivity + mNegativeButtonListener = this@SwitchToManagedProfileForCallActivity + } + setupAlert() + } + + override fun onClick(dialog: DialogInterface?, which: Int) { + if (which == BUTTON_POSITIVE) { + switchToManagedProfile() + } + finish() + } + + private fun switchToManagedProfile() { + try { + applicationContext.startActivityAsUser( + Intent(Intent.ACTION_DIAL, phoneNumber), + ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(), + UserHandle.of(managedProfileUserId) + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to launch activity", e) + } + } + + companion object { + private const val TAG = "SwitchToManagedProfileForCallActivity" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3ecb15b9d79c..fbdfa158a12d 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -26,7 +26,6 @@ import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_INDEX import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_SOURCE; import static com.android.systemui.theme.ThemeOverlayApplier.TIMESTAMP_FIELD; -import android.annotation.Nullable; import android.app.WallpaperColors; import android.app.WallpaperManager; import android.app.WallpaperManager.OnColorsChangedListener; @@ -70,6 +69,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; +import com.android.systemui.monet.TonalPalette; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; @@ -513,39 +513,42 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { /** * Given a color candidate, return an overlay definition. */ - protected @Nullable FabricatedOverlay getOverlay(int color, int type, Style style) { + protected FabricatedOverlay getOverlay(int color, int type, Style style) { boolean nightMode = (mResources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; mColorScheme = new ColorScheme(color, nightMode, style); - List<Integer> colorShades = type == ACCENT - ? mColorScheme.getAllAccentColors() : mColorScheme.getAllNeutralColors(); String name = type == ACCENT ? "accent" : "neutral"; - int paletteSize = mColorScheme.getAccent1().size(); + FabricatedOverlay.Builder overlay = new FabricatedOverlay.Builder("com.android.systemui", name, "android"); - for (int i = 0; i < colorShades.size(); i++) { - int luminosity = i % paletteSize; - int paletteIndex = i / paletteSize + 1; - String resourceName; - switch (luminosity) { - case 0: - resourceName = "android:color/system_" + name + paletteIndex + "_10"; - break; - case 1: - resourceName = "android:color/system_" + name + paletteIndex + "_50"; - break; - default: - int l = luminosity - 1; - resourceName = "android:color/system_" + name + paletteIndex + "_" + l + "00"; - } - overlay.setResourceValue(resourceName, TypedValue.TYPE_INT_COLOR_ARGB8, - ColorUtils.setAlphaComponent(colorShades.get(i), 0xFF)); + + if (type == ACCENT) { + assignTonalPaletteToOverlay("accent1", overlay, mColorScheme.getAccent1()); + assignTonalPaletteToOverlay("accent2", overlay, mColorScheme.getAccent2()); + assignTonalPaletteToOverlay("accent3", overlay, mColorScheme.getAccent3()); + } else { + assignTonalPaletteToOverlay("neutral1", overlay, mColorScheme.getNeutral1()); + assignTonalPaletteToOverlay("neutral2", overlay, mColorScheme.getNeutral2()); } return overlay.build(); } + private void assignTonalPaletteToOverlay(String name, + FabricatedOverlay.Builder overlay, TonalPalette tonalPalette) { + + String resourcePrefix = "android:color/system_" + name; + int colorDataType = TypedValue.TYPE_INT_COLOR_ARGB8; + + tonalPalette.getAllShadesMapped().forEach((key, value) -> { + String resourceName = resourcePrefix + "_" + key; + int colorValue = ColorUtils.setAlphaComponent(value, 0xFF); + overlay.setResourceValue(resourceName, colorDataType, + colorValue); + }); + } + /** * Checks if the color scheme in mColorScheme matches the current system palettes. * @param managedProfiles List of managed profiles for this user. @@ -557,15 +560,15 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { Resources res = userHandle.isSystem() ? mResources : mContext.createContextAsUser(userHandle, 0).getResources(); if (!(res.getColor(android.R.color.system_accent1_500, mContext.getTheme()) - == mColorScheme.getAccent1().get(6) + == mColorScheme.getAccent1().getS500() && res.getColor(android.R.color.system_accent2_500, mContext.getTheme()) - == mColorScheme.getAccent2().get(6) + == mColorScheme.getAccent2().getS500() && res.getColor(android.R.color.system_accent3_500, mContext.getTheme()) - == mColorScheme.getAccent3().get(6) + == mColorScheme.getAccent3().getS500() && res.getColor(android.R.color.system_neutral1_500, mContext.getTheme()) - == mColorScheme.getNeutral1().get(6) + == mColorScheme.getNeutral1().getS500() && res.getColor(android.R.color.system_neutral2_500, mContext.getTheme()) - == mColorScheme.getNeutral2().get(6))) { + == mColorScheme.getNeutral2().getS500())) { return false; } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt index 7726d09cf971..8214822f0335 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt @@ -3,26 +3,43 @@ package com.android.systemui.unfold import android.os.SystemProperties import android.os.VibrationEffect import android.os.Vibrator +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.unfold.updates.FoldProvider.FoldCallback +import java.util.concurrent.Executor import javax.inject.Inject -/** - * Class that plays a haptics effect during unfolding a foldable device - */ +/** Class that plays a haptics effect during unfolding a foldable device */ @SysUIUnfoldScope class UnfoldHapticsPlayer @Inject constructor( unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, + foldProvider: FoldProvider, + @Main private val mainExecutor: Executor, private val vibrator: Vibrator? ) : TransitionProgressListener { + private var isFirstAnimationAfterUnfold = false + init { if (vibrator != null) { // We don't need to remove the callback because we should listen to it // the whole time when SystemUI process is alive unfoldTransitionProgressProvider.addCallback(this) } + + foldProvider.registerCallback( + object : FoldCallback { + override fun onFoldUpdated(isFolded: Boolean) { + if (isFolded) { + isFirstAnimationAfterUnfold = true + } + } + }, + mainExecutor + ) } private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN @@ -36,6 +53,13 @@ constructor( } override fun onTransitionFinishing() { + // Run haptics only when unfolding the device (first animation after unfolding) + if (!isFirstAnimationAfterUnfold) { + return + } + + isFirstAnimationAfterUnfold = false + // Run haptics only if the animation is long enough to notice if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) { playHaptics() diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 59ad24a3e7bb..2709da38a7d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -17,6 +17,9 @@ package com.android.systemui.unfold import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.SystemUnfoldSharedModule @@ -32,6 +35,7 @@ import dagger.Lazy import dagger.Module import dagger.Provides import java.util.Optional +import java.util.concurrent.Executor import javax.inject.Named import javax.inject.Singleton @@ -40,6 +44,20 @@ class UnfoldTransitionModule { @Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui" + /** A globally available FoldStateListener that allows one to query the fold state. */ + @Provides + @Singleton + fun providesFoldStateListener( + deviceStateManager: DeviceStateManager, + @Application context: Context, + @Main executor: Executor + ): DeviceStateManager.FoldStateListener { + val listener = DeviceStateManager.FoldStateListener(context) + deviceStateManager.registerCallback(executor, listener) + + return listener + } + @Provides @Singleton fun providesFoldStateLoggingProvider( 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 d7b0971d9126..c0ba3cc352b0 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 @@ -115,9 +115,7 @@ constructor( private val callbackMutex = Mutex() private val callbacks = mutableSetOf<UserCallback>() private val userInfos: Flow<List<UserInfo>> = - repository.userInfos.map { userInfos -> - userInfos.filter { it.isFull } - } + repository.userInfos.map { userInfos -> userInfos.filter { it.isFull } } /** List of current on-device users to select from. */ val users: Flow<List<UserModel>> @@ -445,7 +443,8 @@ constructor( ) ) } - UserActionModel.ADD_SUPERVISED_USER -> + UserActionModel.ADD_SUPERVISED_USER -> { + dismissDialog() activityStarter.startActivity( Intent() .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) @@ -453,6 +452,7 @@ constructor( .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), /* dismissShade= */ true, ) + } UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> activityStarter.startActivity( Intent(Settings.ACTION_USER_SETTINGS), @@ -493,7 +493,7 @@ constructor( fun showUserSwitcher(context: Context, expandable: Expandable) { if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog) + showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) return } 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 85c29647719b..14cc3e783fed 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,11 +18,13 @@ package com.android.systemui.user.domain.model import android.os.UserHandle +import com.android.systemui.animation.Expandable import com.android.systemui.qs.user.UserSwitchDialogController /** Encapsulates a request to show a dialog. */ sealed class ShowDialogRequestModel( open val dialogShower: UserSwitchDialogController.DialogShower? = null, + open val expandable: Expandable? = null, ) { data class ShowAddUserDialog( val userHandle: UserHandle, @@ -45,5 +47,7 @@ sealed class ShowDialogRequestModel( ) : ShowDialogRequestModel(dialogShower) /** Show the user switcher dialog */ - object ShowUserSwitcherDialog : ShowDialogRequestModel() + data class ShowUserSwitcherDialog( + override val expandable: Expandable?, + ) : ShowDialogRequestModel() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt new file mode 100644 index 000000000000..3fe2a7b19851 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt @@ -0,0 +1,33 @@ +/* + * 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.user.ui.dialog + +import android.app.Dialog +import android.content.DialogInterface +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower + +/** Extracted from [UserSwitchDialogController] */ +class DialogShowerImpl( + private val animateFrom: Dialog, + private val dialogLaunchAnimator: DialogLaunchAnimator, +) : DialogInterface by animateFrom, DialogShower { + override fun showDialog(dialog: Dialog, cuj: DialogCuj) { + dialogLaunchAnimator.showFromDialog(dialog, animateFrom = animateFrom, cuj) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt index ed2589889435..b8ae257aaac5 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt @@ -60,6 +60,7 @@ class UserSwitchDialog( setView(gridFrame) adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) + adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } companion object { 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 41410542204c..79721b370c21 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 @@ -66,12 +66,6 @@ constructor( private fun startHandlingDialogShowRequests() { applicationScope.get().launch { interactor.get().dialogShowRequests.filterNotNull().collect { request -> - currentDialog?.let { - if (it.isShowing) { - it.cancel() - } - } - val (dialog, dialogCuj) = when (request) { is ShowDialogRequestModel.ShowAddUserDialog -> @@ -133,7 +127,10 @@ constructor( } currentDialog = dialog - if (request.dialogShower != null && dialogCuj != null) { + val controller = request.expandable?.dialogLaunchController(dialogCuj) + if (controller != null) { + dialogLaunchAnimator.get().show(dialog, controller) + } else if (request.dialogShower != null && dialogCuj != null) { request.dialogShower?.showDialog(dialog, dialogCuj) } else { dialog.show() diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index fa3c73a26f7b..9a37b0d29028 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1489,6 +1489,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, .setDuration(mDialogHideAnimationDurationMs) .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) .withEndAction(() -> mHandler.postDelayed(() -> { + mController.notifyVisible(false); mDialog.dismiss(); tryToRemoveCaptionsTooltip(); mIsAnimatingDismiss = false; @@ -1499,7 +1500,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, animator.setListener(getJankListener(getDialogView(), TYPE_DISMISS, mDialogHideAnimationDurationMs)).start(); checkODICaptionsTooltip(true); - mController.notifyVisible(false); synchronized (mSafetyWarningLock) { if (mSafetyWarning != null) { if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt new file mode 100644 index 000000000000..7b8235acb0a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wallet.controller + +import android.Manifest +import android.content.Context +import android.content.IntentFilter +import android.service.quickaccesswallet.GetWalletCardsError +import android.service.quickaccesswallet.GetWalletCardsResponse +import android.service.quickaccesswallet.QuickAccessWalletClient +import android.service.quickaccesswallet.WalletCard +import com.android.systemui.broadcast.BroadcastDispatcher +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.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.shareIn + +@SysUISingleton +class WalletContextualSuggestionsController +@Inject +constructor( + @Application private val applicationCoroutineScope: CoroutineScope, + private val walletController: QuickAccessWalletController, + broadcastDispatcher: BroadcastDispatcher, + featureFlags: FeatureFlags +) { + private val allWalletCards: Flow<List<WalletCard>> = + if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { + conflatedCallbackFlow { + val callback = + object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { + override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { + trySendWithFailureLogging(response.walletCards, TAG) + } + + override fun onWalletCardRetrievalError(error: GetWalletCardsError) { + trySendWithFailureLogging(emptyList<WalletCard>(), TAG) + } + } + + walletController.setupWalletChangeObservers( + callback, + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + walletController.updateWalletPreference() + walletController.queryWalletCards(callback) + + awaitClose { + walletController.unregisterWalletChangeObservers( + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + } + } + } else { + emptyFlow() + } + + private val contextualSuggestionsCardIds: Flow<Set<String>> = + if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) { + broadcastDispatcher.broadcastFlow( + filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS), + permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE, + flags = Context.RECEIVER_EXPORTED + ) { intent, _ -> + if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) { + intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet() + } else { + emptySet() + } + } + } else { + emptyFlow() + } + + val contextualSuggestionCards: Flow<List<WalletCard>> = + combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids -> + cards.filter { card -> ids.contains(card.cardId) } + } + .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly) + + companion object { + private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS = + "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS" + + private const val UPDATE_CARD_IDS_EXTRA = "cardIds" + + private const val TAG = "WalletSuggestions" + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index e8f8e25364b3..c76b127a161c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -15,6 +15,7 @@ */ package com.android.keyguard +import com.android.systemui.statusbar.CommandQueue import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner import android.view.View @@ -81,8 +82,10 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View @Mock private lateinit var transitionRepository: KeyguardTransitionRepository + @Mock private lateinit var commandQueue: CommandQueue private lateinit var repository: FakeKeyguardRepository - @Mock private lateinit var logBuffer: LogBuffer + @Mock private lateinit var smallLogBuffer: LogBuffer + @Mock private lateinit var largeLogBuffer: LogBuffer private lateinit var underTest: ClockEventController @Before @@ -99,7 +102,7 @@ class ClockEventControllerTest : SysuiTestCase() { repository = FakeKeyguardRepository() underTest = ClockEventController( - KeyguardInteractor(repository = repository), + KeyguardInteractor(repository = repository, commandQueue = commandQueue), KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, @@ -109,7 +112,8 @@ class ClockEventControllerTest : SysuiTestCase() { context, mainExecutor, bgExecutor, - logBuffer, + smallLogBuffer, + largeLogBuffer, featureFlags ) underTest.clock = clock diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index c8e753844c64..9a9acf3dd986 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -48,6 +48,7 @@ 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.log.LogBuffer; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.shared.clocks.ClockRegistry; @@ -115,6 +116,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; + @Mock + private LogBuffer mLogBuffer; private final View mFakeSmartspaceView = new View(mContext); @@ -156,7 +159,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController + mClockEventController, + mLogBuffer ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 254f9531ef83..8dc1e8fba600 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -189,6 +190,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -198,6 +200,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -212,6 +215,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -223,6 +227,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 84f6d913b310..075ef9df9664 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -405,6 +405,13 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test + public void onBouncerVisibilityChanged_resetsScale() { + mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.INVISIBLE); + + verify(mView).resetScale(); + } + + @Test public void onStartingToHide_sideFpsHintShown_sideFpsHintHidden() { setupGetSecurityView(); setupConditionsToEnableSideFpsHint(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 36ed669e299c..1bbc19931c21 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -49,6 +49,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import androidx.constraintlayout.widget.ConstraintSet; import androidx.test.filters.SmallTest; @@ -357,6 +359,27 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); } + @Test + public void testPlayBackAnimation() { + OnBackAnimationCallback backCallback = mKeyguardSecurityContainer.getBackCallback(); + backCallback.onBackStarted(createBackEvent(0, 0)); + mKeyguardSecurityContainer.getBackCallback().onBackProgressed( + createBackEvent(0, 1)); + assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo( + KeyguardSecurityContainer.MIN_BACK_SCALE); + assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo( + KeyguardSecurityContainer.MIN_BACK_SCALE); + + // reset scale + mKeyguardSecurityContainer.resetScale(); + assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo(1); + assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo(1); + } + + private BackEvent createBackEvent(float touchX, float progress) { + return new BackEvent(0, 0, progress, BackEvent.EDGE_LEFT); + } + private Configuration configuration(@Configuration.Orientation int orientation) { Configuration config = new Configuration(); config.orientation = orientation; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index be4bbdf5adbc..dfad15d68375 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -24,6 +24,7 @@ import android.graphics.Rect; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.ClockAnimations; @@ -65,6 +66,8 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { ScreenOffAnimationController mScreenOffAnimationController; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor; + @Mock + KeyguardLogger mKeyguardLogger; private KeyguardStatusViewController mController; @@ -81,7 +84,8 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { mConfigurationController, mDozeParameters, mFeatureFlags, - mScreenOffAnimationController); + mScreenOffAnimationController, + mKeyguardLogger); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 13cd328d00e0..df6752a2b69d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -32,6 +32,9 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.google.common.truth.Truth.assertThat; @@ -92,6 +95,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.dreams.IDreamManager; import android.service.trust.TrustAgentService; import android.telephony.ServiceState; @@ -116,6 +120,7 @@ import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated; import com.android.keyguard.logging.KeyguardUpdateMonitorLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.log.SessionTracker; @@ -123,6 +128,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -142,6 +148,7 @@ import org.mockito.internal.util.reflection.FieldSetter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; @@ -191,6 +198,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private DevicePolicyManager mDevicePolicyManager; @Mock + private DevicePostureController mDevicePostureController; + @Mock private IDreamManager mDreamManager; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -233,6 +242,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private GlobalSettings mGlobalSettings; private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + @Mock + private FingerprintInteractiveToAuthProvider mInteractiveToAuthProvider; private final int mCurrentUserId = 100; @@ -296,6 +307,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(new ServiceState()); when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN); mMockitoSession = ExtendedMockito.mockitoSession() .spyStatic(SubscriptionManager.class) @@ -307,6 +319,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mContext.getOrCreateTestableResources().addOverride( + com.android.systemui.R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN); mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( mContext.getResources(), mGlobalSettings, @@ -1250,7 +1265,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled() throws RemoteException { // SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); @@ -1258,12 +1273,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - // WHEN require screen on to auth is disabled, and keyguard is not awake - when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0); - mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); - - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); + // WHEN require interactive to auth is disabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); // Preconditions for sfps auth to run keyguardNotGoingAway(); @@ -1279,9 +1290,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // THEN we should listen for sfps when screen off, because require screen on is disabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); - // WHEN require screen on to auth is enabled, and keyguard is not awake - when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1); - mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + // WHEN require interactive to auth is enabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); @@ -1295,6 +1305,61 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } + @Test + public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is enabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should NOT listen for sfps because device is going to sleep + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + } + + @Test + public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is disabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps because screen on to auth is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( @FingerprintSensorProperties.SensorType int sensorType) { @@ -2187,6 +2252,54 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(true)); } + @Test + public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Should not listen for face when posture state in DEVICE_POSTURE_OPENED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Should listen for face when posture state in DEVICE_POSTURE_CLOSED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + + @Test + public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + private void userDeviceLockDown() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId)) @@ -2266,6 +2379,14 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START); } + private void deviceInPostureStateOpened() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED); + } + + private void deviceInPostureStateClosed() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED); + } + private void successfulFingerprintAuth() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationSucceeded( @@ -2407,7 +2528,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, - mFaceWakeUpTriggersConfig); + mFaceWakeUpTriggersConfig, mDevicePostureController, + Optional.of(mInteractiveToAuthProvider)); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java index e4c41a7ed804..05bd1e482950 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -49,6 +49,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -91,6 +92,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { protected @Mock AuthRippleController mAuthRippleController; protected @Mock FeatureFlags mFeatureFlags; protected @Mock KeyguardTransitionRepository mTransitionRepository; + protected @Mock CommandQueue mCommandQueue; protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); protected LockIconViewController mUnderTest; @@ -157,7 +159,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { mAuthRippleController, mResources, new KeyguardTransitionInteractor(mTransitionRepository), - new KeyguardInteractor(new FakeKeyguardRepository()), + new KeyguardInteractor(new FakeKeyguardRepository(), mCommandQueue), mFeatureFlags ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index 7c1e384f8c30..cac4a0e5432c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -12,11 +12,13 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager +import android.widget.FrameLayout import android.widget.LinearLayout import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.DecorView import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNotNull @@ -205,25 +207,74 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { verify(interactionJankMonitor).end(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN) } + @Test + fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisible() { + val touchSurface = createTouchSurface() + + // View is VISIBLE when starting the animation. + runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE } + + // View is invisible while the dialog is shown. + val dialog = showDialogFromView(touchSurface) + assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) + + // View is visible again when the dialog is dismissed. + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + assertThat(touchSurface.visibility).isEqualTo(View.VISIBLE) + } + + @Test + fun testAnimationDoesNotChangeLaunchableViewVisibility_viewInvisible() { + val touchSurface = createTouchSurface() + + // View is INVISIBLE when starting the animation. + runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.INVISIBLE } + + // View is INVISIBLE while the dialog is shown. + val dialog = showDialogFromView(touchSurface) + assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) + + // View is invisible like it was before showing the dialog. + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) + } + + @Test + fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisibleThenGone() { + val touchSurface = createTouchSurface() + + // View is VISIBLE when starting the animation. + runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE } + + // View is INVISIBLE while the dialog is shown. + val dialog = showDialogFromView(touchSurface) + assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) + + // Some external call makes the View GONE. It remains INVISIBLE while the dialog is shown, + // as all visibility changes should be blocked. + runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.GONE } + assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) + + // View is restored to GONE once the dialog is dismissed. + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + assertThat(touchSurface.visibility).isEqualTo(View.GONE) + } + private fun createAndShowDialog( animator: DialogLaunchAnimator = dialogLaunchAnimator, ): TestDialog { val touchSurface = createTouchSurface() - return runOnMainThreadAndWaitForIdleSync { - val dialog = TestDialog(context) - animator.showFromView(dialog, touchSurface) - dialog - } + return showDialogFromView(touchSurface, animator) } private fun createTouchSurface(): View { return runOnMainThreadAndWaitForIdleSync { val touchSurfaceRoot = LinearLayout(context) - val touchSurface = View(context) + val touchSurface = TouchSurfaceView(context) touchSurfaceRoot.addView(touchSurface) // We need to attach the root to the window manager otherwise the exit animation will - // be skipped + // be skipped. ViewUtils.attachView(touchSurfaceRoot) attachedViews.add(touchSurfaceRoot) @@ -231,6 +282,17 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { } } + private fun showDialogFromView( + touchSurface: View, + animator: DialogLaunchAnimator = dialogLaunchAnimator, + ): TestDialog { + return runOnMainThreadAndWaitForIdleSync { + val dialog = TestDialog(context) + animator.showFromView(dialog, touchSurface) + dialog + } + } + private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog { return runOnMainThreadAndWaitForIdleSync { val dialog = TestDialog(context) @@ -248,6 +310,22 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { return result } + private class TouchSurfaceView(context: Context) : FrameLayout(context), LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } + } + private class TestDialog(context: Context) : Dialog(context) { companion object { const val DIALOG_WIDTH = 100 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 83bf1834989b..ace0ccb6a25b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -739,7 +739,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testForwardsDozeEvents() throws RemoteException { when(mStatusBarStateController.isDozing()).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); - mAuthController.setBiometicContextListener(mContextListener); + mAuthController.setBiometricContextListener(mContextListener); mStatusBarStateListenerCaptor.getValue().onDozingChanged(true); mStatusBarStateListenerCaptor.getValue().onDozingChanged(false); @@ -754,7 +754,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testForwardsWakeEvents() throws RemoteException { when(mStatusBarStateController.isDozing()).thenReturn(false); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); - mAuthController.setBiometicContextListener(mContextListener); + mAuthController.setBiometricContextListener(mContextListener); mWakefullnessObserverCaptor.getValue().onStartedGoingToSleep(); mWakefullnessObserverCaptor.getValue().onFinishedGoingToSleep(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index 3c40835fe59d..b92c5d039d45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -53,6 +53,7 @@ import androidx.test.filters.SmallTest import com.airbnb.lottie.LottieAnimationView import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.SysuiTestableContext import com.android.systemui.dump.DumpManager import com.android.systemui.recents.OverviewProxyService import com.android.systemui.util.concurrency.FakeExecutor @@ -106,8 +107,7 @@ class SideFpsControllerTest : SysuiTestCase() { enum class DeviceConfig { X_ALIGNED, - Y_ALIGNED_UNFOLDED, - Y_ALIGNED_FOLDED + Y_ALIGNED, } private lateinit var deviceConfig: DeviceConfig @@ -143,6 +143,7 @@ class SideFpsControllerTest : SysuiTestCase() { private fun testWithDisplay( deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation: Boolean = false, initInfo: DisplayInfo.() -> Unit = {}, windowInsets: WindowInsets = insetsForSmallNavbar(), block: () -> Unit @@ -151,27 +152,21 @@ class SideFpsControllerTest : SysuiTestCase() { when (deviceConfig) { DeviceConfig.X_ALIGNED -> { - displayWidth = 2560 - displayHeight = 1600 - sensorLocation = SensorLocationInternal("", 2325, 0, 0) - boundsWidth = 160 - boundsHeight = 84 + displayWidth = 3000 + displayHeight = 1500 + sensorLocation = SensorLocationInternal("", 2500, 0, 0) + boundsWidth = 200 + boundsHeight = 100 } - DeviceConfig.Y_ALIGNED_UNFOLDED -> { - displayWidth = 2208 - displayHeight = 1840 - sensorLocation = SensorLocationInternal("", 0, 510, 0) - boundsWidth = 110 - boundsHeight = 210 - } - DeviceConfig.Y_ALIGNED_FOLDED -> { - displayWidth = 1080 - displayHeight = 2100 - sensorLocation = SensorLocationInternal("", 0, 590, 0) - boundsWidth = 110 - boundsHeight = 210 + DeviceConfig.Y_ALIGNED -> { + displayWidth = 2500 + displayHeight = 2000 + sensorLocation = SensorLocationInternal("", 0, 300, 0) + boundsWidth = 100 + boundsHeight = 200 } } + indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight) displayBounds = Rect(0, 0, displayWidth, displayHeight) var locations = listOf(sensorLocation) @@ -194,8 +189,10 @@ class SideFpsControllerTest : SysuiTestCase() { val displayInfo = DisplayInfo() displayInfo.initInfo() + val dmGlobal = mock(DisplayManagerGlobal::class.java) val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS) + whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo) whenEver(windowManager.defaultDisplay).thenReturn(display) whenEver(windowManager.maximumWindowMetrics) @@ -203,9 +200,15 @@ class SideFpsControllerTest : SysuiTestCase() { whenEver(windowManager.currentWindowMetrics) .thenReturn(WindowMetrics(displayBounds, windowInsets)) + val sideFpsControllerContext = context.createDisplayContext(display) as SysuiTestableContext + sideFpsControllerContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_reverseDefaultRotation, + isReverseDefaultRotation + ) + sideFpsController = SideFpsController( - context.createDisplayContext(display), + sideFpsControllerContext, layoutInflater, fingerprintManager, windowManager, @@ -299,108 +302,316 @@ class SideFpsControllerTest : SysuiTestCase() { } @Test - fun showsWithTaskbar() = - testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_0 }) { - hidesWithTaskbar(visible = true) - } + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_0() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbarOnY() = + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_90() = testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, - { rotation = Surface.ROTATION_0 } - ) { hidesWithTaskbar(visible = true) } + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_90 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbar90() = - testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_90 }) { - hidesWithTaskbar(visible = true) - } + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_180() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_180 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_180() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_180 }, + windowInsets = insetsForSmallNavbar() + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbar90OnY() = + fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_180() = testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_180 }, + windowInsets = insetsForLargeNavbar() + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) } + + @Test + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_270() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_270 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_0() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_0 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_90() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, { rotation = Surface.ROTATION_90 } - ) { hidesWithTaskbar(visible = true) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbar180() = + fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_InReverseDefaultRotation_90() = testWithDisplay( deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_90 }, + windowInsets = insetsForSmallNavbar() + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_InReverseDefaultRotation_90() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_90 }, + windowInsets = insetsForLargeNavbar() + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) } + + @Test + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_180() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, { rotation = Surface.ROTATION_180 } - ) { hidesWithTaskbar(visible = true) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbar270OnY() = + fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_270() = testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, { rotation = Surface.ROTATION_270 } - ) { hidesWithTaskbar(visible = true) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbarCollapsedDown() = + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_0() = testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_90() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_90 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_180() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_180 }, + ) { + verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) + } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_270() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_270 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_270() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, { rotation = Surface.ROTATION_270 }, windowInsets = insetsForSmallNavbar() - ) { hidesWithTaskbar(visible = true) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun showsWithTaskbarCollapsedDownOnY() = + fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_270() = testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_270 }, + windowInsets = insetsForLargeNavbar() + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_0() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_0 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_90() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_90 }, + ) { + verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) + } + + @Test + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_180() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_180 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + + @Test + fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_InReverseDefaultRotation_180() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, { rotation = Surface.ROTATION_180 }, windowInsets = insetsForSmallNavbar() - ) { hidesWithTaskbar(visible = true) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } @Test - fun hidesWithTaskbarDown() = + fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_InReverseDefaultRotation_180() = testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, { rotation = Surface.ROTATION_180 }, windowInsets = insetsForLargeNavbar() - ) { hidesWithTaskbar(visible = false) } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) } @Test - fun hidesWithTaskbarDownOnY() = + fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_270() = testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, - { rotation = Surface.ROTATION_270 }, - windowInsets = insetsForLargeNavbar() - ) { hidesWithTaskbar(visible = true) } + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } - private fun hidesWithTaskbar(visible: Boolean) { + private fun verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible: Boolean) { + sideFpsController.overlayOffsets = sensorLocation overlayController.show(SENSOR_ID, REASON_UNKNOWN) executor.runAllReady() - sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false) + sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(true, false) executor.runAllReady() verify(windowManager).addView(any(), any()) verify(windowManager, never()).removeView(any()) - verify(sideFpsView).visibility = if (visible) View.VISIBLE else View.GONE + verify(sideFpsView).visibility = if (sfpsViewVisible) View.VISIBLE else View.GONE } + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, + * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device + * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement + * in other rotations have been omitted. + */ @Test - fun testIndicatorPlacementForXAlignedSensor() = - testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED) { - overlayController.show(SENSOR_ID, REASON_UNKNOWN) + fun verifiesIndicatorPlacementForXAlignedSensor_0() { + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) executor.runAllReady() verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) + } + } + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 + * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct + * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works + * correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() { + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) } + } + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, + * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device + * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement + * in other rotations have been omitted. + */ @Test - fun testIndicatorPlacementForYAlignedSensor() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { + fun verifiesIndicatorPlacementForYAlignedSensor_0() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) + } + + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 + * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct + * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works + * correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + overlayController.show(SENSOR_ID, REASON_UNKNOWN) executor.runAllReady() @@ -412,7 +623,6 @@ class SideFpsControllerTest : SysuiTestCase() { @Test fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay { // By default all those tests assume the side fps sensor is available. - assertThat(fingerprintManager.hasSideFpsSensor()).isTrue() } @@ -425,7 +635,7 @@ class SideFpsControllerTest : SysuiTestCase() { @Test fun testLayoutParams_isKeyguardDialogType() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { sideFpsController.overlayOffsets = sensorLocation sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) overlayController.show(SENSOR_ID, REASON_UNKNOWN) @@ -440,7 +650,7 @@ class SideFpsControllerTest : SysuiTestCase() { @Test fun testLayoutParams_hasNoMoveAnimationWindowFlag() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { sideFpsController.overlayOffsets = sensorLocation sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) overlayController.show(SENSOR_ID, REASON_UNKNOWN) @@ -455,7 +665,7 @@ class SideFpsControllerTest : SysuiTestCase() { @Test fun testLayoutParams_hasTrustedOverlayWindowFlag() = - testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) { + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { sideFpsController.overlayOffsets = sensorLocation sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) overlayController.show(SENSOR_ID, REASON_UNKNOWN) 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 53bc2c231d0c..1ef119d7fb16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -52,7 +53,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -95,7 +95,6 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var transitionController: LockscreenShadeTransitionController @Mock private lateinit var configurationController: ConfigurationController - @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController @@ -106,7 +105,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator @Mock private lateinit var featureFlags: FeatureFlags - @Mock private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor + @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor + @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } @@ -138,10 +138,10 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context, fingerprintManager, inflater, windowManager, accessibilityManager, statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, - configurationController, systemClock, keyguardStateController, + configurationController, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator, featureFlags, - mPrimaryBouncerInteractor, isDebuggable + primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, ) block() } 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 b061eb395119..0c34e54d2ec4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -81,6 +81,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -202,6 +203,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private SinglePointerTouchProcessor mSinglePointerTouchProcessor; + @Mock + private AlternateBouncerInteractor mAlternateBouncerInteractor; // Capture listeners so that they can be used to send events @Captor @@ -292,7 +295,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mDisplayManager, mHandler, mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor, - mPrimaryBouncerInteractor, mSinglePointerTouchProcessor); + mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, + mAlternateBouncerInteractor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -406,7 +410,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // GIVEN overlay was showing and the udfps bouncer is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN the overlay is hidden mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java index 3c61382d9446..9c32c38e665c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java @@ -30,6 +30,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionChangeEvent; @@ -43,7 +44,6 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.mockito.ArgumentCaptor; @@ -73,9 +73,9 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator; protected @Mock KeyguardBouncer mBouncer; protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor; + protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor; protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); - protected FakeSystemClock mSystemClock = new FakeSystemClock(); protected UdfpsKeyguardViewController mController; @@ -86,10 +86,6 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor; protected List<ShadeExpansionListener> mExpansionListeners; - private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateBouncer> - mAlternateBouncerCaptor; - protected StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; - private @Captor ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor; protected KeyguardStateController.Callback mKeyguardStateControllerCallback; @@ -135,12 +131,6 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { } } - protected void captureAltAuthInterceptor() { - verify(mStatusBarKeyguardViewManager).setAlternateBouncer( - mAlternateBouncerCaptor.capture()); - mAlternateBouncer = mAlternateBouncerCaptor.getValue(); - } - protected void captureKeyguardStateControllerCallback() { verify(mKeyguardStateController).addCallback( mKeyguardStateControllerCallbackCaptor.capture()); @@ -160,6 +150,7 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { protected UdfpsKeyguardViewController createUdfpsKeyguardViewController( boolean useModernBouncer, boolean useExpandedOverlay) { mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer); + mFeatureFlags.set(Flags.MODERN_ALTERNATE_BOUNCER, useModernBouncer); mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay); when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn( useModernBouncer ? null : mBouncer); @@ -172,14 +163,14 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { mDumpManager, mLockscreenShadeTransitionController, mConfigurationController, - mSystemClock, mKeyguardStateController, mUnlockedScreenOffAnimationController, mDialogManager, mUdfpsController, mActivityLaunchAnimator, mFeatureFlags, - mPrimaryBouncerInteractor); + mPrimaryBouncerInteractor, + mAlternateBouncerInteractor); return controller; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index babe5334e3eb..7715f7fc5dc9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -19,25 +19,22 @@ package com.android.systemui.biometrics; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper.RunWithLooper; +import android.testing.TestableLooper; import android.view.MotionEvent; import androidx.test.filters.SmallTest; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.shade.ShadeExpansionListener; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.phone.KeyguardBouncer; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,11 +43,12 @@ import org.mockito.Captor; @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper + +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest { - private @Captor ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> + private @Captor ArgumentCaptor<PrimaryBouncerExpansionCallback> mBouncerExpansionCallbackCaptor; - private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback; + private PrimaryBouncerExpansionCallback mBouncerExpansionCallback; @Override public UdfpsKeyguardViewController createUdfpsKeyguardViewController() { @@ -72,8 +70,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController assertTrue(mController.shouldPauseAuth()); } - - @Test public void testRegistersExpansionChangedListenerOnAttached() { mController.onViewAttached(); @@ -237,85 +233,9 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController public void testOverrideShouldPauseAuthOnShadeLocked() { mController.onViewAttached(); captureStatusBarStateListeners(); - captureAltAuthInterceptor(); sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED); assertTrue(mController.shouldPauseAuth()); - - mAlternateBouncer.showAlternateBouncer(); // force show - assertFalse(mController.shouldPauseAuth()); - assertTrue(mAlternateBouncer.isShowingAlternateBouncer()); - - mAlternateBouncer.hideAlternateBouncer(); // stop force show - assertTrue(mController.shouldPauseAuth()); - assertFalse(mAlternateBouncer.isShowingAlternateBouncer()); - } - - @Test - public void testOnDetachedStateReset() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // WHEN view is detached - mController.onViewDetached(); - - // THEN remove alternate auth interceptor - verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAlternateBouncer); - } - - @Test - public void testHiddenUdfpsBouncerOnTouchOutside_nothingHappens() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer isn't showing - mAlternateBouncer.hideAlternateBouncer(); - - // WHEN touch is observed outside the view - mController.onTouchOutsideView(); - - // THEN bouncer / alt auth methods are never called - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean()); - } - - @Test - public void testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer is showing - mAlternateBouncer.showAlternateBouncer(); - - // WHEN touch is observed outside the view 200ms later (just within threshold) - mSystemClock.advanceTime(200); - mController.onTouchOutsideView(); - - // THEN bouncer / alt auth methods are never called because not enough time has passed - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean()); - } - - @Test - public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showPrimaryBouncer() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer is showing - mAlternateBouncer.showAlternateBouncer(); - - // WHEN touch is observed outside the view 205ms later - mSystemClock.advanceTime(205); - mController.onTouchOutsideView(); - - // THEN show the bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(eq(true)); } @Test @@ -334,25 +254,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController } @Test - public void testShowUdfpsBouncer() { - // GIVEN view is attached and status bar expansion is 0 - mController.onViewAttached(); - captureStatusBarExpansionListeners(); - captureKeyguardStateControllerCallback(); - captureAltAuthInterceptor(); - updateStatusBarExpansion(0, true); - reset(mView); - when(mView.getContext()).thenReturn(mResourceContext); - when(mResourceContext.getString(anyInt())).thenReturn("test string"); - - // WHEN status bar expansion is 0 but udfps bouncer is requested - mAlternateBouncer.showAlternateBouncer(); - - // THEN alpha is 255 - verify(mView).setUnpausedAlpha(255); - } - - @Test public void testTransitionToFullShadeProgress() { // GIVEN view is attached and status bar expansion is 1f mController.onViewAttached(); @@ -370,24 +271,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController } @Test - public void testShowUdfpsBouncer_transitionToFullShadeProgress() { - // GIVEN view is attached and status bar expansion is 1f - mController.onViewAttached(); - captureStatusBarExpansionListeners(); - captureKeyguardStateControllerCallback(); - captureAltAuthInterceptor(); - updateStatusBarExpansion(1f, true); - mAlternateBouncer.showAlternateBouncer(); - reset(mView); - - // WHEN we're transitioning to the full shade - mController.setTransitionToFullShadeProgress(1.0f); - - // THEN alpha is 255 (b/c udfps bouncer is requested) - verify(mView).setUnpausedAlpha(255); - } - - @Test public void testUpdatePanelExpansion_pauseAuth() { // GIVEN view is attached + on the keyguard mController.onViewAttached(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt index 2d412dcaa909..9060922266c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt @@ -21,26 +21,35 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView +import com.android.systemui.keyguard.data.repository.BiometricRepository import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.phone.KeyguardBouncer import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -57,6 +66,7 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle keyguardBouncerRepository = KeyguardBouncerRepository( mock(com.android.keyguard.ViewMediatorCallback::class.java), + FakeSystemClock(), TestCoroutineScope(), bouncerLogger, ) @@ -77,15 +87,43 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle mock(KeyguardBypassController::class.java), mKeyguardUpdateMonitor ) + mAlternateBouncerInteractor = + AlternateBouncerInteractor( + keyguardBouncerRepository, + mock(BiometricRepository::class.java), + mock(SystemClock::class.java), + mock(KeyguardUpdateMonitor::class.java), + mock(FeatureFlags::class.java) + ) return createUdfpsKeyguardViewController( /* useModernBouncer */ true, /* useExpandedOverlay */ false ) } - /** After migration, replaces LockIconViewControllerTest version */ @Test - fun testShouldPauseAuthBouncerShowing() = + fun shadeLocked_showAlternateBouncer_unpauseAuth() = + runBlocking(IMMEDIATE) { + // GIVEN view is attached + on the SHADE_LOCKED (udfps view not showing) + mController.onViewAttached() + captureStatusBarStateListeners() + sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED) + + // WHEN alternate bouncer is requested + val job = mController.listenForAlternateBouncerVisibility(this) + keyguardBouncerRepository.setAlternateVisible(true) + yield() + + // THEN udfps view will animate in & pause auth is updated to NOT pause + verify(mView).animateInUdfpsBouncer(any()) + assertFalse(mController.shouldPauseAuth()) + + job.cancel() + } + + /** After migration to MODERN_BOUNCER, replaces UdfpsKeyguardViewControllerTest version */ + @Test + fun shouldPauseAuthBouncerShowing() = runBlocking(IMMEDIATE) { // GIVEN view attached and we're on the keyguard mController.onViewAttached() @@ -95,7 +133,7 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle // WHEN the bouncer expansion is VISIBLE val job = mController.listenForBouncerExpansion(this) keyguardBouncerRepository.setPrimaryVisible(true) - keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE) + keyguardBouncerRepository.setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) yield() // THEN UDFPS shouldPauseAuth == true diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt index d550b927154c..8255a1452bd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -80,15 +80,6 @@ class UdfpsViewTest : SysuiTestCase() { } @Test - fun forwardsEvents() { - view.dozeTimeTick() - verify(animationViewController).dozeTimeTick() - - view.onTouchOutsideView() - verify(animationViewController).onTouchOutsideView() - } - - @Test fun layoutSizeFitsSensor() { val params = withArgCaptor<RectF> { verify(animationViewController).onSensorRectUpdated(capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt new file mode 100644 index 000000000000..af46d9b97abf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Point +import android.graphics.Rect +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` as whenEver + +@SmallTest +@RunWith(Parameterized::class) +class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { + val underTest = spy(EllipseOverlapDetector(neededPoints = 1)) + + @Before + fun setUp() { + // Use one single center point for testing, required or total number of points may change + whenEver(underTest.calculateSensorPoints(SENSOR)) + .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY()))) + } + + @Test + fun isGoodOverlap() { + val touchData = + TOUCH_DATA.copy( + x = testCase.x.toFloat(), + y = testCase.y.toFloat(), + minor = testCase.minor, + major = testCase.major + ) + val actual = underTest.isGoodOverlap(touchData, SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase( + val x: Int, + val y: Int, + val minor: Float, + val major: Float, + val expected: Boolean + ) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + innerYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 300f, + major = 300f, + expected = true + ), + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right), + innerYs = listOf(SENSOR.top, SENSOR.bottom), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 100f, + major = 100f, + expected = false + ) + ) + .flatten() + } +} + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 0f // used for perfect circles +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [NormalizedTouchData]. */ +private val TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */) + +private fun genTestCases( + innerXs: List<Int>, + innerYs: List<Int>, + outerXs: List<Int>, + outerYs: List<Int>, + minor: Float, + major: Float, + expected: Boolean +): List<EllipseOverlapDetectorTest.TestCase> { + return (innerXs + outerXs).flatMap { x -> + (innerYs + outerYs).map { y -> + EllipseOverlapDetectorTest.TestCase(x, y, minor, major, expected) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index 95c53b408056..34ddf795c7e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -39,7 +39,8 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() @Test fun processTouch() { - overlapDetector.shouldReturn = testCase.isGoodOverlap + overlapDetector.shouldReturn = + testCase.currentPointers.associate { pointer -> pointer.id to pointer.onSensor } val actual = underTest.processTouch( @@ -56,7 +57,7 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() data class TestCase( val event: MotionEvent, - val isGoodOverlap: Boolean, + val currentPointers: List<TestPointer>, val previousPointerOnSensorId: Int, val overlayParams: UdfpsOverlayParams, val expected: TouchProcessorResult, @@ -91,28 +92,21 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() genPositiveTestCases( motionEventAction = MotionEvent.ACTION_DOWN, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = true, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.DOWN, - expectedPointerOnSensorId = POINTER_ID, - ), - genPositiveTestCases( - motionEventAction = MotionEvent.ACTION_DOWN, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = true, - expectedInteractionEvent = InteractionEvent.DOWN, - expectedPointerOnSensorId = POINTER_ID, + expectedPointerOnSensorId = POINTER_ID_1, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_DOWN, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = false, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.UNCHANGED, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_DOWN, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = false, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.UP, expectedPointerOnSensorId = INVALID_POINTER_ID, ), @@ -120,107 +114,232 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() genPositiveTestCases( motionEventAction = MotionEvent.ACTION_MOVE, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = true, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.DOWN, - expectedPointerOnSensorId = POINTER_ID, + expectedPointerOnSensorId = POINTER_ID_1, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_MOVE, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = true, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.UNCHANGED, - expectedPointerOnSensorId = POINTER_ID, + expectedPointerOnSensorId = POINTER_ID_1, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_MOVE, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = false, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.UNCHANGED, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_MOVE, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = false, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.UP, expectedPointerOnSensorId = INVALID_POINTER_ID, ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = INVALID_POINTER_ID, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = true) + ), + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID_2, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = true) + ), + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID_2, + ), // MotionEvent.ACTION_UP genPositiveTestCases( motionEventAction = MotionEvent.ACTION_UP, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = true, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.UP, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_UP, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = true, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.UP, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_UP, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = false, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.UNCHANGED, expectedPointerOnSensorId = INVALID_POINTER_ID, ), - genPositiveTestCases( - motionEventAction = MotionEvent.ACTION_UP, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = false, - expectedInteractionEvent = InteractionEvent.UP, - expectedPointerOnSensorId = INVALID_POINTER_ID, - ), // MotionEvent.ACTION_CANCEL genPositiveTestCases( motionEventAction = MotionEvent.ACTION_CANCEL, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = true, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.CANCEL, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_CANCEL, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = true, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)), expectedInteractionEvent = InteractionEvent.CANCEL, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_CANCEL, previousPointerOnSensorId = INVALID_POINTER_ID, - isGoodOverlap = false, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.CANCEL, expectedPointerOnSensorId = INVALID_POINTER_ID, ), genPositiveTestCases( motionEventAction = MotionEvent.ACTION_CANCEL, - previousPointerOnSensorId = POINTER_ID, - isGoodOverlap = false, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)), expectedInteractionEvent = InteractionEvent.CANCEL, expectedPointerOnSensorId = INVALID_POINTER_ID, ), + // MotionEvent.ACTION_POINTER_DOWN + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_DOWN + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = INVALID_POINTER_ID, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = true), + TestPointer(id = POINTER_ID_2, onSensor = false) + ), + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID_1, + ), + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_DOWN + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = INVALID_POINTER_ID, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = true) + ), + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID_2 + ), + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_DOWN + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = true), + TestPointer(id = POINTER_ID_2, onSensor = false) + ), + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID_1, + ), + // MotionEvent.ACTION_POINTER_UP + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_UP + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = INVALID_POINTER_ID, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = false) + ), + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID + ), + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_UP + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = POINTER_ID_2, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = true) + ), + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_POINTER_UP, + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = true), + TestPointer(id = POINTER_ID_2, onSensor = false) + ), + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID + ), + genPositiveTestCases( + motionEventAction = + MotionEvent.ACTION_POINTER_UP + + (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT), + previousPointerOnSensorId = POINTER_ID_1, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = true), + TestPointer(id = POINTER_ID_2, onSensor = false) + ), + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID_1 + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_POINTER_UP, + previousPointerOnSensorId = POINTER_ID_2, + currentPointers = + listOf( + TestPointer(id = POINTER_ID_1, onSensor = false), + TestPointer(id = POINTER_ID_2, onSensor = true) + ), + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID_2 + ) ) .flatten() + listOf( - // Unsupported MotionEvent actions. - genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_DOWN), - genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_UP), genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_ENTER), genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_MOVE), - genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT), + genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT) ) .flatten() } } +data class TestPointer(val id: Int, val onSensor: Boolean) + /* Display dimensions in native resolution and natural orientation. */ private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400 private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600 +/* Placeholder touch parameters. */ +private const val POINTER_ID_1 = 42 +private const val POINTER_ID_2 = 43 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.2345f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + /* * ROTATION_0 map: * _ _ _ _ @@ -244,6 +363,7 @@ private val ROTATION_0_NATIVE_SENSOR_BOUNDS = private val ROTATION_0_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_0, + nativeOrientation = ORIENTATION, nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 250f, @@ -271,6 +391,7 @@ private val ROTATION_90_NATIVE_SENSOR_BOUNDS = private val ROTATION_90_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_90, + nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 150f, @@ -304,25 +425,18 @@ private val ROTATION_270_NATIVE_SENSOR_BOUNDS = private val ROTATION_270_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_270, + nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 450f, nativeYOutsideSensor = 250f, ) -/* Placeholder touch parameters. */ -private const val POINTER_ID = 42 -private const val NATIVE_MINOR = 2.71828f -private const val NATIVE_MAJOR = 3.14f -private const val ORIENTATION = 1.23f -private const val TIME = 12345699L -private const val GESTURE_START = 12345600L - /* Template [MotionEvent]. */ private val MOTION_EVENT = obtainMotionEvent( action = 0, - pointerId = POINTER_ID, + pointerId = POINTER_ID_1, x = 0f, y = 0f, minor = 0f, @@ -335,7 +449,7 @@ private val MOTION_EVENT = /* Template [NormalizedTouchData]. */ private val NORMALIZED_TOUCH_DATA = NormalizedTouchData( - POINTER_ID, + POINTER_ID_1, x = 0f, y = 0f, NATIVE_MINOR, @@ -352,6 +466,7 @@ private val NORMALIZED_TOUCH_DATA = */ private data class OrientationBasedInputs( @Rotation val rotation: Int, + val nativeOrientation: Float, val nativeXWithinSensor: Float, val nativeYWithinSensor: Float, val nativeXOutsideSensor: Float, @@ -380,7 +495,7 @@ private data class OrientationBasedInputs( private fun genPositiveTestCases( motionEventAction: Int, previousPointerOnSensorId: Int, - isGoodOverlap: Boolean, + currentPointers: List<TestPointer>, expectedInteractionEvent: InteractionEvent, expectedPointerOnSensorId: Int ): List<SinglePointerTouchProcessorTest.TestCase> { @@ -395,21 +510,47 @@ private fun genPositiveTestCases( return scaleFactors.flatMap { scaleFactor -> orientations.map { orientation -> val overlayParams = orientation.toOverlayParams(scaleFactor) - val nativeX = orientation.getNativeX(isGoodOverlap) - val nativeY = orientation.getNativeY(isGoodOverlap) + + val pointerProperties = + currentPointers + .map { pointer -> + val pp = MotionEvent.PointerProperties() + pp.id = pointer.id + pp + } + .toList() + + val pointerCoords = + currentPointers + .map { pointer -> + val pc = MotionEvent.PointerCoords() + pc.x = orientation.getNativeX(pointer.onSensor) * scaleFactor + pc.y = orientation.getNativeY(pointer.onSensor) * scaleFactor + pc.touchMinor = NATIVE_MINOR * scaleFactor + pc.touchMajor = NATIVE_MAJOR * scaleFactor + pc.orientation = orientation.nativeOrientation + pc + } + .toList() + val event = MOTION_EVENT.copy( action = motionEventAction, - x = nativeX * scaleFactor, - y = nativeY * scaleFactor, - minor = NATIVE_MINOR * scaleFactor, - major = NATIVE_MAJOR * scaleFactor, + pointerProperties = pointerProperties, + pointerCoords = pointerCoords ) + val expectedTouchData = - NORMALIZED_TOUCH_DATA.copy( - x = ROTATION_0_INPUTS.getNativeX(isGoodOverlap), - y = ROTATION_0_INPUTS.getNativeY(isGoodOverlap), - ) + if (expectedPointerOnSensorId != INVALID_POINTER_ID) { + NORMALIZED_TOUCH_DATA.copy( + pointerId = expectedPointerOnSensorId, + x = ROTATION_0_INPUTS.getNativeX(isWithinSensor = true), + y = ROTATION_0_INPUTS.getNativeY(isWithinSensor = true) + ) + } else { + NormalizedTouchData() + } + val expected = TouchProcessorResult.ProcessedTouch( event = expectedInteractionEvent, @@ -418,7 +559,7 @@ private fun genPositiveTestCases( ) SinglePointerTouchProcessorTest.TestCase( event = event, - isGoodOverlap = isGoodOverlap, + currentPointers = currentPointers, previousPointerOnSensorId = previousPointerOnSensorId, overlayParams = overlayParams, expected = expected, @@ -431,7 +572,7 @@ private fun genTestCasesForUnsupportedAction( motionEventAction: Int ): List<SinglePointerTouchProcessorTest.TestCase> { val isGoodOverlap = true - val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID) + val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID_1) return previousPointerOnSensorIds.map { previousPointerOnSensorId -> val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f) val nativeX = ROTATION_0_INPUTS.getNativeX(isGoodOverlap) @@ -446,7 +587,7 @@ private fun genTestCasesForUnsupportedAction( ) SinglePointerTouchProcessorTest.TestCase( event = event, - isGoodOverlap = isGoodOverlap, + currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = isGoodOverlap)), previousPointerOnSensorId = previousPointerOnSensorId, overlayParams = overlayParams, expected = TouchProcessorResult.Failure(), @@ -473,13 +614,23 @@ private fun obtainMotionEvent( pc.touchMinor = minor pc.touchMajor = major pc.orientation = orientation + return obtainMotionEvent(action, arrayOf(pp), arrayOf(pc), time, gestureStart) +} + +private fun obtainMotionEvent( + action: Int, + pointerProperties: Array<MotionEvent.PointerProperties>, + pointerCoords: Array<MotionEvent.PointerCoords>, + time: Long, + gestureStart: Long, +): MotionEvent { return MotionEvent.obtain( gestureStart /* downTime */, time /* eventTime */, action /* action */, - 1 /* pointerCount */, - arrayOf(pp) /* pointerProperties */, - arrayOf(pc) /* pointerCoords */, + pointerCoords.size /* pointerCount */, + pointerProperties /* pointerProperties */, + pointerCoords /* pointerCoords */, 0 /* metaState */, 0 /* buttonState */, 1f /* xPrecision */, @@ -503,4 +654,19 @@ private fun MotionEvent.copy( gestureStart: Long = this.downTime, ) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart) +private fun MotionEvent.copy( + action: Int = this.action, + pointerProperties: List<MotionEvent.PointerProperties>, + pointerCoords: List<MotionEvent.PointerCoords>, + time: Long = this.eventTime, + gestureStart: Long = this.downTime +) = + obtainMotionEvent( + action, + pointerProperties.toTypedArray(), + pointerCoords.toTypedArray(), + time, + gestureStart + ) + private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index 0fadc138637a..e4df754ec96a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -106,6 +106,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mClassifiers.add(mClassifierB); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, @@ -121,6 +122,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue(); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index 4281ee0f139f..ae38eb67c431 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -89,25 +89,27 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { mClassifiers.add(mClassifierA); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, mAccessibilityManager, false, mFakeFeatureFlags); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test public void testA11yDisablesGesture() { - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); } @Test public void testA11yDisablesTap() { - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } @@ -179,4 +181,11 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { when(mFalsingDataProvider.isA11yAction()).thenReturn(true); assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } + + @Test + public void testSkipUnfolded() { + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + when(mFalsingDataProvider.isFolded()).thenReturn(false); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java index 5fa7214f07ff..94cf384267ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -38,6 +39,7 @@ public class ClassifierTest extends SysuiTestCase { private float mOffsetY = 0; @Mock private BatteryController mBatteryController; + private FoldStateListener mFoldStateListener = new FoldStateListener(mContext); private final DockManagerFake mDockManager = new DockManagerFake(); public void setup() { @@ -47,7 +49,8 @@ public class ClassifierTest extends SysuiTestCase { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index d315c2da0703..c451a1e754c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -50,6 +51,8 @@ public class FalsingDataProviderTest extends ClassifierTest { private FalsingDataProvider mDataProvider; @Mock private BatteryController mBatteryController; + @Mock + private FoldStateListener mFoldStateListener; private final DockManagerFake mDockManager = new DockManagerFake(); @Before @@ -61,7 +64,8 @@ public class FalsingDataProviderTest extends ClassifierTest { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After @@ -316,4 +320,16 @@ public class FalsingDataProviderTest extends ClassifierTest { mDataProvider.onA11yAction(); assertThat(mDataProvider.isA11yAction()).isTrue(); } + + @Test + public void test_FoldedState_Folded() { + when(mFoldStateListener.getFolded()).thenReturn(true); + assertThat(mDataProvider.isFolded()).isTrue(); + } + + @Test + public void test_FoldedState_Unfolded() { + when(mFoldStateListener.getFolded()).thenReturn(false); + assertThat(mDataProvider.isFolded()).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt new file mode 100644 index 000000000000..3e6cc3bb4f6b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt @@ -0,0 +1,66 @@ +/* + * 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.compose + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.ViewUtils +import android.widget.FrameLayout +import androidx.compose.ui.platform.ComposeView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ComposeInitializerTest : SysuiTestCase() { + @Test + fun testCanAddComposeViewInInitializedWindow() { + if (!ComposeFacade.isComposeAvailable()) { + return + } + + val root = TestWindowRoot(context) + try { + runOnMainThreadAndWaitForIdleSync { ViewUtils.attachView(root) } + assertThat(root.isAttachedToWindow).isTrue() + + runOnMainThreadAndWaitForIdleSync { root.addView(ComposeView(context)) } + } finally { + runOnMainThreadAndWaitForIdleSync { ViewUtils.detachView(root) } + } + } + + private fun runOnMainThreadAndWaitForIdleSync(f: () -> Unit) { + mContext.mainExecutor.execute(f) + waitForIdleSync() + } + + class TestWindowRoot(context: Context) : FrameLayout(context) { + override fun onAttachedToWindow() { + super.onAttachedToWindow() + ComposeFacade.composeInitializer().onAttachedToWindow(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ComposeFacade.composeInitializer().onDetachedFromWindow(this) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt index 0a81c38e7448..ebbe096b0da3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt @@ -269,6 +269,14 @@ class ControlsBindingControllerImplTest : SysuiTestCase() { } @Test + fun testBindServiceForPanel() { + controller.bindServiceForPanel(TEST_COMPONENT_NAME_1) + executor.runAllReady() + + verify(providers[0]).bindServiceForPanel() + } + + @Test fun testSubscribe() { val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN) val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index 1b34706bd220..25f471b0d3e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -919,6 +919,12 @@ class ControlsControllerImplTest : SysuiTestCase() { .getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier) assertThat(userStructure.file).isNotNull() } + + @Test + fun testBindForPanel() { + controller.bindComponentForPanel(TEST_COMPONENT) + verify(bindingController).bindServiceForPanel(TEST_COMPONENT) + } } private class DidRunRunnable() : Runnable { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt index af3f24a1c58a..da548f7ccef2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt @@ -105,6 +105,22 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { } @Test + fun testBindForPanel() { + manager.bindServiceForPanel() + executor.runAllReady() + assertTrue(context.isBound(componentName)) + } + + @Test + fun testUnbindPanelIsUnbound() { + manager.bindServiceForPanel() + executor.runAllReady() + manager.unbindService() + executor.runAllReady() + assertFalse(context.isBound(componentName)) + } + + @Test fun testNullBinding() { val mockContext = mock(Context::class.java) lateinit var serviceConnection: ServiceConnection diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index d172c9a2e630..edc6882e71c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -229,6 +229,15 @@ class ControlsUiControllerImplTest : SysuiTestCase() { } @Test + fun testPanelBindsForPanel() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + setUpPanel(panel) + + underTest.show(parent, {}, context) + verify(controlsController).bindComponentForPanel(panel.componentName) + } + + @Test fun testPanelCallsTaskViewFactoryCreate() { mockLayoutInflater() val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) 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 d910a275967b..e87f1042927f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -144,7 +144,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSensorDebounce() { - mDozeSensors.setListening(true, true); + mDozeSensors.setListening(true, true, true); mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class)); mTestableLooper.processAllMessages(); @@ -162,7 +162,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSetListening_firstTrue_registerSettingsObserver() { verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); - mDozeSensors.setListening(true, true); + mDozeSensors.setListening(true, true, true); verify(mTriggerSensor).registerSettingsObserver(any(ContentObserver.class)); } @@ -170,8 +170,8 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSetListening_twiceTrue_onlyRegisterSettingsObserverOnce() { verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); - mDozeSensors.setListening(true, true); - mDozeSensors.setListening(true, true); + mDozeSensors.setListening(true, true, true); + mDozeSensors.setListening(true, true, true); verify(mTriggerSensor, times(1)).registerSettingsObserver(any(ContentObserver.class)); } @@ -196,7 +196,7 @@ public class DozeSensorsTest extends SysuiTestCase { assertFalse(mSensorTap.mRequested); // WHEN we're now in a low powered state - dozeSensors.setListening(true, true, true); + dozeSensors.setListeningWithPowerState(true, true, true, true); // THEN the tap sensor is registered assertTrue(mSensorTap.mRequested); @@ -207,12 +207,12 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensors enabled when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true); - // GIVEN a trigger sensor + // GIVEN a trigger sensor that's enabled by settings Sensor mockSensor = mock(Sensor.class); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled( mockSensor, - /* settingEnabled */ true, - /* requiresTouchScreen */ true); + /* settingEnabled */ true + ); when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor))) .thenReturn(true); @@ -228,12 +228,12 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensors enabled when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true); - // GIVEN a trigger sensor + // GIVEN a trigger sensor that's not enabled by settings Sensor mockSensor = mock(Sensor.class); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled( mockSensor, - /* settingEnabled*/ false, - /* requiresTouchScreen */ true); + /* settingEnabled*/ false + ); when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor))) .thenReturn(true); @@ -249,12 +249,12 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensors enabled when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true); - // GIVEN a trigger sensor that's + // GIVEN a trigger sensor that's not enabled by settings Sensor mockSensor = mock(Sensor.class); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled( mockSensor, - /* settingEnabled*/ false, - /* requiresTouchScreen */ true); + /* settingEnabled*/ false + ); when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor))) .thenReturn(true); @@ -264,7 +264,7 @@ public class DozeSensorsTest extends SysuiTestCase { // WHEN ignoreSetting is called triggerSensor.ignoreSetting(true); - // THEN the sensor is registered + // THEN the sensor is still registered since the setting is ignore assertTrue(triggerSensor.mRegistered); } @@ -275,10 +275,10 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN a trigger sensor Sensor mockSensor = mock(Sensor.class); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled( mockSensor, - /* settingEnabled*/ true, - /* requiresTouchScreen */ true); + /* settingEnabled*/ true + ); when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor))) .thenReturn(true); @@ -295,7 +295,7 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensor that supports postures Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT); Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture( new Sensor[] { null /* unknown */, closedSensor, @@ -316,7 +316,7 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensor that supports postures Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT); Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture( new Sensor[] { null /* unknown */, closedSensor, @@ -345,7 +345,7 @@ public class DozeSensorsTest extends SysuiTestCase { // GIVEN doze sensor that supports postures Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT); Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture( new Sensor[] { null /* unknown */, closedSensor, @@ -400,7 +400,7 @@ public class DozeSensorsTest extends SysuiTestCase { public void testUdfpsEnrollmentChanged() throws Exception { // GIVEN a UDFPS_LONG_PRESS trigger sensor that's not configured Sensor mockSensor = mock(Sensor.class); - TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture( mockSensor, REASON_SENSOR_UDFPS_LONG_PRESS, /* configured */ false); @@ -409,7 +409,7 @@ public class DozeSensorsTest extends SysuiTestCase { .thenReturn(true); // WHEN listening state is set to TRUE - mDozeSensors.setListening(true, true); + mDozeSensors.setListening(true, true, true); // THEN mRegistered is still false b/c !mConfigured assertFalse(triggerSensor.mConfigured); @@ -439,6 +439,35 @@ public class DozeSensorsTest extends SysuiTestCase { } @Test + public void aodOnlySensor_onlyRegisteredWhenAodSensorsIncluded() { + // GIVEN doze sensors enabled + when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true); + + // GIVEN a trigger sensor that requires aod + Sensor mockSensor = mock(Sensor.class); + TriggerSensor aodOnlyTriggerSensor = mDozeSensors.createDozeSensorRequiringAod(mockSensor); + when(mSensorManager.requestTriggerSensor(eq(aodOnlyTriggerSensor), eq(mockSensor))) + .thenReturn(true); + mDozeSensors.addSensor(aodOnlyTriggerSensor); + + // WHEN aod only sensors aren't included + mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true, + /* includeAodOnlySensors */false); + + // THEN the sensor is not registered or requested + assertFalse(aodOnlyTriggerSensor.mRequested); + assertFalse(aodOnlyTriggerSensor.mRegistered); + + // WHEN aod only sensors ARE included + mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true, + /* includeAodOnlySensors */true); + + // THEN the sensor is registered and requested + assertTrue(aodOnlyTriggerSensor.mRequested); + assertTrue(aodOnlyTriggerSensor.mRegistered); + } + + @Test public void liftToWake_defaultSetting_configDefaultFalse() { // WHEN the default lift to wake gesture setting is false when(mResources.getBoolean( @@ -494,8 +523,8 @@ public class DozeSensorsTest extends SysuiTestCase { mTriggerSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap}; } - public TriggerSensor createDozeSensor(Sensor sensor, boolean settingEnabled, - boolean requiresTouchScreen) { + public TriggerSensor createDozeSensorWithSettingEnabled(Sensor sensor, + boolean settingEnabled) { return new TriggerSensor(/* sensor */ sensor, /* setting name */ "test_setting", /* settingDefault */ settingEnabled, @@ -504,11 +533,13 @@ public class DozeSensorsTest extends SysuiTestCase { /* reportsTouchCoordinate*/ false, /* requiresTouchscreen */ false, /* ignoresSetting */ false, - requiresTouchScreen, - /* immediatelyReRegister */ true); + /* requiresProx */ false, + /* immediatelyReRegister */ true, + /* requiresAod */false + ); } - public TriggerSensor createDozeSensor( + public TriggerSensor createDozeSensorForPosture( Sensor sensor, int pulseReason, boolean configured @@ -522,15 +553,35 @@ public class DozeSensorsTest extends SysuiTestCase { /* requiresTouchscreen */ false, /* ignoresSetting */ false, /* requiresTouchScreen */ false, - /* immediatelyReRegister*/ true); + /* immediatelyReRegister*/ true, + false + ); } /** - * create a doze sensor that supports postures and is enabled + * Create a doze sensor that requires Aod */ - public TriggerSensor createDozeSensor(Sensor[] sensors, int posture) { + public TriggerSensor createDozeSensorRequiringAod(Sensor sensor) { + return new TriggerSensor(/* sensor */ sensor, + /* setting name */ "aod_requiring_sensor", + /* settingDefault */ true, + /* configured */ true, + /* pulseReason*/ 0, + /* reportsTouchCoordinate*/ false, + /* requiresTouchscreen */ false, + /* ignoresSetting */ false, + /* requiresProx */ false, + /* immediatelyReRegister */ true, + /* requiresAoD */ true + ); + } + + /** + * Create a doze sensor that supports postures and is enabled + */ + public TriggerSensor createDozeSensorForPosture(Sensor[] sensors, int posture) { return new TriggerSensor(/* sensor */ sensors, - /* setting name */ "test_setting", + /* setting name */ "posture_test_setting", /* settingDefault */ true, /* configured */ true, /* pulseReason*/ 0, @@ -539,7 +590,9 @@ public class DozeSensorsTest extends SysuiTestCase { /* ignoresSetting */ true, /* requiresProx */ false, /* immediatelyReRegister */ true, - posture); + posture, + /* requiresUi */ false + ); } public void addSensor(TriggerSensor sensor) { 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 b66a454638ac..3552399586a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -395,6 +395,14 @@ public class DozeTriggersTest extends SysuiTestCase { verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat()); } + + @Test + public void udfpsLongPress_dozeState_notRegistered() { + // GIVEN device is DOZE_AOD_PAUSED + when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE); + // beverlyt + } + private void waitForSensorManager() { mExecutor.runAllReady(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 2799a25316d0..ff883eb16bde 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -37,9 +37,9 @@ import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.complication.ComplicationHostViewController; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.phone.KeyguardBouncer; -import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import org.junit.Before; diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java index 4bd53c00327f..f64179deec35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java @@ -41,11 +41,11 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; -import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.wm.shell.animation.FlingAnimationUtils; @@ -302,12 +302,13 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float velocityY = -1; swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); - verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_HIDDEN)); + verify(mValueAnimatorCreator).create(eq(expansion), + eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); verify(mValueAnimator, never()).addListener(any()); verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion), - eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN), + eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN), eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); verify(mValueAnimator).start(); verify(mUiEventLogger, never()).log(any()); @@ -324,7 +325,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float velocityY = 1; swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); - verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE)); + verify(mValueAnimatorCreator).create(eq(expansion), + eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor = ArgumentCaptor.forClass(AnimatorListenerAdapter.class); @@ -332,7 +334,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue(); verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion), - eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE), + eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE), eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); verify(mValueAnimator).start(); verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED); @@ -355,12 +357,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncer.EXPANSION_VISIBLE)); + eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); verify(mValueAnimator, never()).addListener(any()); verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE), + eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE), eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); verify(mValueAnimator).start(); verify(mUiEventLogger, never()).log(any()); @@ -381,12 +383,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncer.EXPANSION_HIDDEN)); + eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); verify(mValueAnimator, never()).addListener(any()); verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN), + eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN), eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); verify(mValueAnimator).start(); verify(mUiEventLogger, never()).log(any()); @@ -405,7 +407,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float velocityY = -1; swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); - verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE)); + verify(mValueAnimatorCreator).create(eq(expansion), + eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor = ArgumentCaptor.forClass(AnimatorListenerAdapter.class); @@ -413,7 +416,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue(); verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion), - eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE), + eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE), eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); verify(mValueAnimator).start(); verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 465976607a01..0a03b2c87f71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any @@ -86,9 +87,9 @@ class CustomizationProviderTest : SysuiTestCase() { @Mock private lateinit var backgroundHandler: Handler @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage @Mock private lateinit var launchAnimator: DialogLaunchAnimator + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: CustomizationProvider - private lateinit var testScope: TestScope @Before @@ -160,6 +161,7 @@ class CustomizationProviderTest : SysuiTestCase() { keyguardInteractor = KeyguardInteractor( repository = FakeKeyguardRepository(), + commandQueue = commandQueue, ), registry = mock(), lockPatternUtils = lockPatternUtils, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 9b0d8dbe5a65..f55b86686152 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -66,6 +67,7 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; @@ -135,6 +137,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock AuthController mAuthController; private @Mock ShadeExpansionStateManager mShadeExpansionStateManager; private @Mock ShadeWindowLogger mShadeWindowLogger; + private @Mock FeatureFlags mFeatureFlags; private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake(); private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); @@ -483,6 +486,38 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { assertTrue(mViewMediator.isShowingAndNotOccluded()); } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileInteractive_resets() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(true); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager).reset(anyBoolean()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(false); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, @@ -510,6 +545,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mScreenOnCoordinator, mInteractionJankMonitor, mDreamOverlayStateController, + mFeatureFlags, () -> mShadeController, () -> mNotificationShadeWindowController, () -> mActivityLaunchAnimator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java index f32d76bb601e..39a453da7f92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -51,7 +52,12 @@ public class WakefulnessLifecycleTest extends SysuiTestCase { public void setUp() throws Exception { mWallpaperManager = mock(IWallpaperManager.class); mWakefulness = - new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class)); + new WakefulnessLifecycle( + mContext, + mWallpaperManager, + new FakeSystemClock(), + mock(DumpManager.class) + ); mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class); mWakefulness.addObserver(mWakefulnessObserver); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index 7205f3068abb..8da4eae2f64a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -22,15 +22,21 @@ import android.content.Context import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.camera.CameraGestureHelper +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock +import org.mockito.Mockito.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class CameraQuickAffordanceConfigTest : SysuiTestCase() { @@ -62,4 +68,24 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) } + + @Test + fun `getPickerScreenState - default when launchable`() = runTest { + setLaunchable(true) + + Truth.assertThat(underTest.getPickerScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + } + + @Test + fun `getPickerScreenState - unavailable when not launchable`() = runTest { + setLaunchable(false) + + Truth.assertThat(underTest.getPickerScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) + } + + private fun setLaunchable(isLaunchable: Boolean) { + whenever(cameraGestureHelper.canCameraGestureBeLaunched(anyInt())).thenReturn(isLaunchable) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index 7c10108d5b45..15b85ded5fd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher @@ -83,169 +84,205 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { settings = FakeSettings() - underTest = DoNotDisturbQuickAffordanceConfig( - context, - zenModeController, - settings, - userTracker, - testDispatcher, - conditionUri, - enableZenModeDialog, - ) + underTest = + DoNotDisturbQuickAffordanceConfig( + context, + zenModeController, + settings, + userTracker, + testDispatcher, + conditionUri, + enableZenModeDialog, + ) } @Test - fun `dnd not available - picker state hidden`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(false) + fun `dnd not available - picker state hidden`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(false) - //when - val result = underTest.getPickerScreenState() + // when + val result = underTest.getPickerScreenState() - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result) - } + // then + assertEquals( + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, + result + ) + } @Test - fun `dnd available - picker state visible`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - - //when - val result = underTest.getPickerScreenState() - - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default, result) - } + fun `dnd available - picker state visible`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + + // when + val result = underTest.getPickerScreenState() + + // then + assertThat(result) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + val defaultPickerState = + result as KeyguardQuickAffordanceConfig.PickerScreenState.Default + assertThat(defaultPickerState.configureIntent).isNotNull() + assertThat(defaultPickerState.configureIntent?.action) + .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) + } @Test - fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - whenever(zenModeController.zen).thenReturn(-1) - settings.putInt(Settings.Secure.ZEN_DURATION, -2) - collectLastValue(underTest.lockScreenState) - runCurrent() - - //when - val result = underTest.onTriggered(null) - verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG)) - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(ZEN_MODE_OFF, spyZenMode.value) - assertNull(spyConditionId.value) - } + fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + whenever(zenModeController.zen).thenReturn(-1) + settings.putInt(Settings.Secure.ZEN_DURATION, -2) + collectLastValue(underTest.lockScreenState) + runCurrent() + + // when + val result = underTest.onTriggered(null) + verify(zenModeController) + .setZen( + spyZenMode.capture(), + spyConditionId.capture(), + eq(DoNotDisturbQuickAffordanceConfig.TAG) + ) + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(ZEN_MODE_OFF, spyZenMode.value) + assertNull(spyConditionId.value) + } @Test - fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is FOREVER - set zen with no condition`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) - settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) - collectLastValue(underTest.lockScreenState) - runCurrent() - - //when - val result = underTest.onTriggered(null) - verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG)) - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) - assertNull(spyConditionId.value) - } + fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting FOREVER - set zen without condition`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) + settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) + collectLastValue(underTest.lockScreenState) + runCurrent() + + // when + val result = underTest.onTriggered(null) + verify(zenModeController) + .setZen( + spyZenMode.capture(), + spyConditionId.capture(), + eq(DoNotDisturbQuickAffordanceConfig.TAG) + ) + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) + assertNull(spyConditionId.value) + } @Test - fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is not FOREVER or PROMPT - set zen with condition`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) - settings.putInt(Settings.Secure.ZEN_DURATION, -900) - collectLastValue(underTest.lockScreenState) - runCurrent() - - //when - val result = underTest.onTriggered(null) - verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG)) - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) - assertEquals(conditionUri, spyConditionId.value) - } + fun `onTriggered - dnd ZEN_MODE_OFF - setting not FOREVER or PROMPT - zen with condition`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) + settings.putInt(Settings.Secure.ZEN_DURATION, -900) + collectLastValue(underTest.lockScreenState) + runCurrent() + + // when + val result = underTest.onTriggered(null) + verify(zenModeController) + .setZen( + spyZenMode.capture(), + spyConditionId.capture(), + eq(DoNotDisturbQuickAffordanceConfig.TAG) + ) + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) + assertEquals(conditionUri, spyConditionId.value) + } @Test - fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() = testScope.runTest { - //given - val expandable: Expandable = mock() - whenever(zenModeController.isZenAvailable).thenReturn(true) - whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) - settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) - whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) - collectLastValue(underTest.lockScreenState) - runCurrent() - - //when - val result = underTest.onTriggered(expandable) - - //then - assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog) - assertEquals(expandable, (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable) - } + fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() = + testScope.runTest { + // given + val expandable: Expandable = mock() + whenever(zenModeController.isZenAvailable).thenReturn(true) + whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) + settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) + whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) + collectLastValue(underTest.lockScreenState) + runCurrent() + + // when + val result = underTest.onTriggered(expandable) + + // then + assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog) + assertEquals( + expandable, + (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable + ) + } @Test - fun `lockScreenState - dndAvailable starts as true - changes to false - State moves to Hidden`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor() - val valueSnapshot = collectLastValue(underTest.lockScreenState) - val secondLastValue = valueSnapshot() - verify(zenModeController).addCallback(callbackCaptor.capture()) - - //when - callbackCaptor.value.onZenAvailableChanged(false) - val lastValue = valueSnapshot() - - //then - assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden) - } + fun `lockScreenState - dndAvailable starts as true - change to false - State is Hidden`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor() + val valueSnapshot = collectLastValue(underTest.lockScreenState) + val secondLastValue = valueSnapshot() + verify(zenModeController).addCallback(callbackCaptor.capture()) + + // when + callbackCaptor.value.onZenAvailableChanged(false) + val lastValue = valueSnapshot() + + // then + assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) + assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden) + } @Test - fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - changes to not OFF - State moves to Visible`() = testScope.runTest { - //given - whenever(zenModeController.isZenAvailable).thenReturn(true) - whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) - val valueSnapshot = collectLastValue(underTest.lockScreenState) - val secondLastValue = valueSnapshot() - val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor() - verify(zenModeController).addCallback(callbackCaptor.capture()) - - //when - callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS) - val lastValue = valueSnapshot() - - //then - assertEquals( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - Icon.Resource( - R.drawable.qs_dnd_icon_off, - ContentDescription.Resource(R.string.dnd_is_off) + fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - change to not OFF - State Visible`() = + testScope.runTest { + // given + whenever(zenModeController.isZenAvailable).thenReturn(true) + whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF) + val valueSnapshot = collectLastValue(underTest.lockScreenState) + val secondLastValue = valueSnapshot() + val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor() + verify(zenModeController).addCallback(callbackCaptor.capture()) + + // when + callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS) + val lastValue = valueSnapshot() + + // then + assertEquals( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_off, + ContentDescription.Resource(R.string.dnd_is_off) + ), + ActivationState.Inactive ), - ActivationState.Inactive - ), - secondLastValue, - ) - assertEquals( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - Icon.Resource( - R.drawable.qs_dnd_icon_on, - ContentDescription.Resource(R.string.dnd_is_on) + secondLastValue, + ) + assertEquals( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_on, + ContentDescription.Resource(R.string.dnd_is_on) + ), + ActivationState.Active ), - ActivationState.Active - ), - lastValue, - ) - } -}
\ No newline at end of file + lastValue, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 6255980601ac..9d2ddffddb5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -141,7 +141,7 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { whenever(controller.isAbleToOpenCameraApp).thenReturn(true) assertThat(underTest.getPickerScreenState()) - .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index d875dd94da3e..ca44fa18f6c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -159,7 +159,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { setUpState() assertThat(underTest.getPickerScreenState()) - .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..805dcec0f5b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import androidx.test.filters.SmallTest +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.SysuiTestCase +import com.android.systemui.camera.CameraIntentsWrapper +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class VideoCameraQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var activityIntentHelper: ActivityIntentHelper + + private lateinit var underTest: VideoCameraQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + VideoCameraQuickAffordanceConfig( + context = context, + cameraIntents = CameraIntentsWrapper(context), + activityIntentHelper = activityIntentHelper, + userTracker = FakeUserTracker(), + ) + } + + @Test + fun `lockScreenState - visible when launchable`() = runTest { + setLaunchable(true) + + val lockScreenState = collectLastValue(underTest.lockScreenState) + + assertThat(lockScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java) + } + + @Test + fun `lockScreenState - hidden when not launchable`() = runTest { + setLaunchable(false) + + val lockScreenState = collectLastValue(underTest.lockScreenState) + + assertThat(lockScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) + } + + @Test + fun `getPickerScreenState - default when launchable`() = runTest { + setLaunchable(true) + + assertThat(underTest.getPickerScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + } + + @Test + fun `getPickerScreenState - unavailable when not launchable`() = runTest { + setLaunchable(false) + + assertThat(underTest.getPickerScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) + } + + private fun setLaunchable(isLaunchable: Boolean) { + whenever( + activityIntentHelper.getTargetActivityInfo( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn( + if (isLaunchable) { + mock() + } else { + null + } + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt new file mode 100644 index 000000000000..a92dd3b92397 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt @@ -0,0 +1,176 @@ +/* + * 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.app.admin.DevicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class BiometricRepositoryTest : SysuiTestCase() { + private lateinit var underTest: BiometricRepository + + @Mock private lateinit var authController: AuthController + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + private lateinit var userRepository: FakeUserRepository + + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private var testableLooper: TestableLooper? = null + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + userRepository = FakeUserRepository() + } + + private suspend fun createBiometricRepository() { + userRepository.setUserInfos(listOf(PRIMARY_USER)) + userRepository.setSelectedUserInfo(PRIMARY_USER) + underTest = + BiometricRepositoryImpl( + context = context, + lockPatternUtils = lockPatternUtils, + broadcastDispatcher = fakeBroadcastDispatcher, + authController = authController, + userRepository = userRepository, + devicePolicyManager = devicePolicyManager, + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + looper = testableLooper!!.looper, + ) + } + + @Test + fun fingerprintEnrollmentChange() = + testScope.runTest { + createBiometricRepository() + val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled) + runCurrent() + + val captor = argumentCaptor<AuthController.Callback>() + verify(authController).addCallback(captor.capture()) + whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true) + captor.value.onEnrollmentsChanged( + BiometricType.UNDER_DISPLAY_FINGERPRINT, + PRIMARY_USER_ID, + true + ) + assertThat(fingerprintEnabledByDevicePolicy()).isTrue() + + whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false) + captor.value.onEnrollmentsChanged( + BiometricType.UNDER_DISPLAY_FINGERPRINT, + PRIMARY_USER_ID, + false + ) + assertThat(fingerprintEnabledByDevicePolicy()).isFalse() + } + + @Test + fun strongBiometricAllowedChange() = + testScope.runTest { + createBiometricRepository() + val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed) + runCurrent() + + val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>() + verify(lockPatternUtils).registerStrongAuthTracker(captor.capture()) + + captor.value + .getStub() + .onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + assertThat(strongBiometricAllowed()).isTrue() + + captor.value + .getStub() + .onStrongAuthRequiredChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + assertThat(strongBiometricAllowed()).isFalse() + } + + @Test + fun fingerprintDisabledByDpmChange() = + testScope.runTest { + createBiometricRepository() + val fingerprintEnabledByDevicePolicy = + collectLastValue(underTest.isFingerprintEnabledByDevicePolicy) + runCurrent() + + whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) + .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) + broadcastDPMStateChange() + assertThat(fingerprintEnabledByDevicePolicy()).isFalse() + + whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())).thenReturn(0) + broadcastDPMStateChange() + assertThat(fingerprintEnabledByDevicePolicy()).isTrue() + } + + private fun broadcastDPMStateChange() { + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED) + ) + } + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt index 9970a6796ed6..969537d23111 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.keyguard.ViewMediatorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.time.SystemClock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import org.junit.Before @@ -34,6 +35,7 @@ import org.mockito.MockitoAnnotations @RunWith(JUnit4::class) class KeyguardBouncerRepositoryTest : SysuiTestCase() { + @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var bouncerLogger: TableLogBuffer lateinit var underTest: KeyguardBouncerRepository @@ -43,7 +45,12 @@ class KeyguardBouncerRepositoryTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val testCoroutineScope = TestCoroutineScope() underTest = - KeyguardBouncerRepository(viewMediatorCallback, testCoroutineScope, bouncerLogger) + KeyguardBouncerRepository( + viewMediatorCallback, + systemClock, + testCoroutineScope, + bouncerLogger, + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index be712f699b7b..f997d18a57a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.common.shared.model.Position +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback @@ -38,14 +39,17 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -68,6 +72,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var dozeParameters: DozeParameters private lateinit var underTest: KeyguardRepositoryImpl @@ -84,6 +89,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, keyguardUpdateMonitor, dozeTransitionListener, + dozeParameters, authController, dreamOverlayCallbackController, ) @@ -170,6 +176,26 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isAodAvailable() = runTest { + val flow = underTest.isAodAvailable + var isAodAvailable = collectLastValue(flow) + runCurrent() + + val callback = + withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) } + + whenever(dozeParameters.getAlwaysOn()).thenReturn(false) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(false) + + whenever(dozeParameters.getAlwaysOn()).thenReturn(true) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(true) + + flow.onCompletion { verify(dozeParameters).removeCallback(callback) } + } + + @Test fun isKeyguardOccluded() = runTest(UnconfinedTestDispatcher()) { whenever(keyguardStateController.isOccluded).thenReturn(false) 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 index 5d2f0eb01de1..32cec09c3580 100644 --- 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 @@ -104,7 +104,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1)) assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN) - val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9)) + val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.1)) assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) job.cancel() @@ -168,6 +168,25 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(wtfHandler.failed).isTrue() } + @Test + fun `Attempt to manually update transition after CANCELED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.2f, TransitionState.CANCELED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + private fun listWithStep( step: BigDecimal, start: BigDecimal = BigDecimal.ZERO, @@ -201,7 +220,10 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { ) ) fractions.forEachIndexed { index, fraction -> - assertThat(steps[index + 1]) + val step = steps[index + 1] + val truncatedValue = + BigDecimal(step.value.toDouble()).setScale(2, RoundingMode.HALF_UP).toFloat() + assertThat(step.copy(value = truncatedValue)) .isEqualTo( TransitionStep( from, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt new file mode 100644 index 000000000000..4b069051423c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.app.trust.TrustManager +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.keyguard.logging.TrustRepositoryLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class TrustRepositoryTest : SysuiTestCase() { + @Mock private lateinit var trustManager: TrustManager + @Captor private lateinit var listenerCaptor: ArgumentCaptor<TrustManager.TrustListener> + private lateinit var userRepository: FakeUserRepository + private lateinit var testScope: TestScope + private val users = listOf(UserInfo(1, "user 1", 0), UserInfo(2, "user 1", 0)) + + private lateinit var underTest: TrustRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope() + userRepository = FakeUserRepository() + userRepository.setUserInfos(users) + + val logger = + TrustRepositoryLogger( + LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false) + ) + underTest = + TrustRepositoryImpl(testScope.backgroundScope, userRepository, trustManager, logger) + } + + @Test + fun isCurrentUserTrusted_whenTrustChanges_emitsLatestValue() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + + val currentUserId = users[0].id + userRepository.setSelectedUserInfo(users[0]) + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + + listener.onTrustChanged(true, false, currentUserId, 0, emptyList()) + assertThat(isCurrentUserTrusted()).isTrue() + + listener.onTrustChanged(false, false, currentUserId, 0, emptyList()) + + assertThat(isCurrentUserTrusted()).isFalse() + } + + @Test + fun isCurrentUserTrusted_isFalse_byDefault() = + testScope.runTest { + runCurrent() + + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + + assertThat(isCurrentUserTrusted()).isFalse() + } + + @Test + fun isCurrentUserTrusted_whenTrustChangesForDifferentUser_noop() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listenerCaptor.capture()) + userRepository.setSelectedUserInfo(users[0]) + val listener = listenerCaptor.value + + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + // current user is trusted. + listener.onTrustChanged(true, true, users[0].id, 0, emptyList()) + // some other user is not trusted. + listener.onTrustChanged(false, false, users[1].id, 0, emptyList()) + + assertThat(isCurrentUserTrusted()).isTrue() + } + + @Test + fun isCurrentUserTrusted_whenTrustChangesForCurrentUser_emitsNewValue() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + userRepository.setSelectedUserInfo(users[0]) + + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + listener.onTrustChanged(true, true, users[0].id, 0, emptyList()) + assertThat(isCurrentUserTrusted()).isTrue() + + listener.onTrustChanged(false, true, users[0].id, 0, emptyList()) + assertThat(isCurrentUserTrusted()).isFalse() + } + + @Test + fun isCurrentUserTrusted_whenUserChangesWithoutRecentTrustChange_defaultsToFalse() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + userRepository.setSelectedUserInfo(users[0]) + listener.onTrustChanged(true, true, users[0].id, 0, emptyList()) + + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + userRepository.setSelectedUserInfo(users[1]) + + assertThat(isCurrentUserTrusted()).isFalse() + } + + @Test + fun isCurrentUserTrusted_trustChangesFirstBeforeUserInfoChanges_emitsCorrectValue() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted) + + listener.onTrustChanged(true, true, users[0].id, 0, emptyList()) + assertThat(isCurrentUserTrusted()).isFalse() + + userRepository.setSelectedUserInfo(users[0]) + + assertThat(isCurrentUserTrusted()).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt new file mode 100644 index 000000000000..1da7241e58bd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt @@ -0,0 +1,156 @@ +/* + * 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 androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.ViewMediatorCallback +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeBiometricRepository +import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class AlternateBouncerInteractorTest : SysuiTestCase() { + private lateinit var underTest: AlternateBouncerInteractor + private lateinit var bouncerRepository: KeyguardBouncerRepository + private lateinit var biometricRepository: FakeBiometricRepository + @Mock private lateinit var systemClock: SystemClock + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var bouncerLogger: TableLogBuffer + private lateinit var featureFlags: FakeFeatureFlags + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + bouncerRepository = + KeyguardBouncerRepository( + mock(ViewMediatorCallback::class.java), + FakeSystemClock(), + TestCoroutineScope(), + bouncerLogger, + ) + biometricRepository = FakeBiometricRepository() + featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) } + underTest = + AlternateBouncerInteractor( + bouncerRepository, + biometricRepository, + systemClock, + keyguardUpdateMonitor, + featureFlags, + ) + } + + @Test + fun canShowAlternateBouncerForFingerprint_givenCanShow() { + givenCanShowAlternateBouncer() + assertTrue(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_alternateBouncerUIUnavailable() { + givenCanShowAlternateBouncer() + bouncerRepository.setAlternateBouncerUIAvailable(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() { + givenCanShowAlternateBouncer() + biometricRepository.setFingerprintEnrolled(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() { + givenCanShowAlternateBouncer() + biometricRepository.setStrongBiometricAllowed(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() { + givenCanShowAlternateBouncer() + biometricRepository.setFingerprintEnabledByDevicePolicy(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun show_whenCanShow() { + givenCanShowAlternateBouncer() + + assertTrue(underTest.show()) + assertTrue(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun show_whenCannotShow() { + givenCannotShowAlternateBouncer() + + assertFalse(underTest.show()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun hide_wasPreviouslyShowing() { + bouncerRepository.setAlternateVisible(true) + + assertTrue(underTest.hide()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun hide_wasNotPreviouslyShowing() { + bouncerRepository.setAlternateVisible(false) + + assertFalse(underTest.hide()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + private fun givenCanShowAlternateBouncer() { + bouncerRepository.setAlternateBouncerUIAvailable(true) + biometricRepository.setFingerprintEnrolled(true) + biometricRepository.setStrongBiometricAllowed(true) + biometricRepository.setFingerprintEnabledByDevicePolicy(true) + } + + private fun givenCannotShowAlternateBouncer() { + biometricRepository.setFingerprintEnrolled(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt new file mode 100644 index 000000000000..68d13d354a43 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.StatusBarManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.CommandQueue.Callbacks +import com.android.systemui.util.mockito.argumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardInteractorTest : SysuiTestCase() { + @Mock private lateinit var commandQueue: CommandQueue + + private lateinit var underTest: KeyguardInteractor + private lateinit var repository: FakeKeyguardRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + repository = FakeKeyguardRepository() + underTest = KeyguardInteractor(repository, commandQueue) + } + + @Test + fun onCameraLaunchDetected() = runTest { + val flow = underTest.onCameraLaunchDetected + var cameraLaunchSource = collectLastValue(flow) + runCurrent() + + val captor = argumentCaptor<CommandQueue.Callbacks>() + verify(commandQueue).addCallback(captor.capture()) + + captor.value.onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) + assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.WIGGLE) + + captor.value.onCameraLaunchGestureDetected( + StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP + ) + assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.POWER_DOUBLE_TAP) + + captor.value.onCameraLaunchGestureDetected( + StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER + ) + assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.LIFT_TRIGGER) + + captor.value.onCameraLaunchGestureDetected( + StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE + ) + assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.QUICK_AFFORDANCE) + + flow.onCompletion { verify(commandQueue).removeCallback(captor.value) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 14b7c311d9af..43287b03b36a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -44,6 +44,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any @@ -216,6 +217,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller @Mock private lateinit var expandable: Expandable @Mock private lateinit var launchAnimator: DialogLaunchAnimator + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -286,7 +288,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ) underTest = KeyguardQuickAffordanceInteractor( - keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), + keyguardInteractor = + KeyguardInteractor( + repository = FakeKeyguardRepository(), + commandQueue = commandQueue + ), registry = FakeKeyguardQuickAffordanceRegistry( mapOf( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 972919a9d27a..b75a15da641a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -46,6 +46,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock @@ -75,6 +76,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var launchAnimator: DialogLaunchAnimator + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -152,7 +154,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceInteractor( - keyguardInteractor = KeyguardInteractor(repository = repository), + keyguardInteractor = + KeyguardInteractor(repository = repository, commandQueue = commandQueue), registry = FakeKeyguardQuickAffordanceRegistry( mapOf( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index d2b7838274a9..5a7a3d49b628 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Interpolators @@ -33,10 +34,12 @@ import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.keyguard.util.KeyguardTransitionRunner import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -54,6 +57,7 @@ import org.mockito.MockitoAnnotations */ @SmallTest @RunWith(JUnit4::class) +@FlakyTest(bugId = 265303901) class KeyguardTransitionScenariosTest : SysuiTestCase() { private lateinit var testScope: TestScope @@ -66,9 +70,14 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // Used to verify transition requests for test output @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository + @Mock private lateinit var commandQueue: CommandQueue private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor + private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor + private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor + private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor + private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor @Before fun setUp() { @@ -85,7 +94,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( scope = testScope, - keyguardInteractor = KeyguardInteractor(keyguardRepository), + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), shadeRepository = shadeRepository, keyguardTransitionRepository = mockTransitionRepository, keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), @@ -95,11 +104,47 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDreamingTransitionInteractor = FromDreamingTransitionInteractor( scope = testScope, - keyguardInteractor = KeyguardInteractor(keyguardRepository), + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), keyguardTransitionRepository = mockTransitionRepository, keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) fromDreamingTransitionInteractor.start() + + fromAodTransitionInteractor = + FromAodTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromAodTransitionInteractor.start() + + fromGoneTransitionInteractor = + FromGoneTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromGoneTransitionInteractor.start() + + fromDozingTransitionInteractor = + FromDozingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDozingTransitionInteractor.start() + + fromOccludedTransitionInteractor = + FromOccludedTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromOccludedTransitionInteractor.start() } @Test @@ -135,7 +180,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardRepository.setDreamingWithOverlay(false) // AND occluded has stopped keyguardRepository.setKeyguardOccluded(false) - runCurrent() + advanceUntilIdle() val info = withArgCaptor<TransitionInfo> { @@ -190,6 +235,332 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `OCCLUDED to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `OCCLUDED to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `DOZING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a prior transition has run to DOZING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to wake + keyguardRepository.setWakefulnessModel(startingToWake()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DOZING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to AOD should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DREAMING`() = + testScope.runTest { + // GIVEN a device that is not dreaming or dozing + keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + reset(mockTransitionRepository) + + // WHEN the device begins to dream + keyguardRepository.setDreamingWithOverlay(true) + advanceUntilIdle() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DREAMING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DREAMING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun startingToWake() = WakefulnessModel( WakefulnessState.STARTING_TO_WAKE, @@ -197,4 +568,12 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { WakeSleepReason.OTHER, WakeSleepReason.OTHER ) + + private fun startingToSleep() = + WakefulnessModel( + WakefulnessState.STARTING_TO_SLEEP, + true, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt index db9c4e713e37..fbfeca9c2a25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.domain.interactor import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.phone.KeyguardBouncer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,8 +33,10 @@ class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() { private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor() @Mock private lateinit var mPrimaryBouncerExpansionCallback: - KeyguardBouncer.PrimaryBouncerExpansionCallback - @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback + PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback + @Mock + private lateinit var keyguardResetCallback: + PrimaryBouncerCallbackInteractor.KeyguardResetCallback @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt index a6fc13bcb011..7f48ea19c91a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -30,11 +30,11 @@ import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN -import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..7fa204bb980b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: GoneToDreamingTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = GoneToDreamingTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeOut() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.1f)) + repository.sendTransitionStep(step(0.2f)) + // ...up to here + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(1f)) + + // Only three values should be present, since the dream overlay runs for a small + // fraction + // of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + // ...up to here + repository.sendTransitionStep(step(1f)) + // And a final reset event on CANCEL + repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED)) + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]).isEqualTo(0f) + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_DREAMING_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "GoneToDreamingTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index a2c2f711b1d4..022afdd61fc2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -47,6 +47,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any @@ -84,6 +85,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var launchAnimator: DialogLaunchAnimator + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: KeyguardBottomAreaViewModel @@ -124,7 +126,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { ) repository = FakeKeyguardRepository() - val keyguardInteractor = KeyguardInteractor(repository = repository) + val keyguardInteractor = + KeyguardInteractor(repository = repository, commandQueue = commandQueue) whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..539fc2c1548e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: LockscreenToDreamingTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = LockscreenToDreamingTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeOut() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.1f)) + repository.sendTransitionStep(step(0.2f)) + // ...up to here + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(1f)) + + // Only three values should be present, since the dream overlay runs for a small + // fraction of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + // ...up to here + repository.sendTransitionStep(step(1f)) + // And a final reset event on FINISHED + repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]).isEqualTo(0f) + + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_DREAMING_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "LockscreenToDreamingTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt new file mode 100644 index 000000000000..759345f51c59 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: LockscreenToOccludedTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = LockscreenToOccludedTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeOut() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.1f)) + repository.sendTransitionStep(step(0.4f)) + // ...up to here + repository.sendTransitionStep(step(0.7f)) + repository.sendTransitionStep(step(1f)) + + // Only 3 values should be present, since the dream overlay runs for a small fraction + // of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(1f - animValue(0.4f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(1f)) + // ...up to here + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(1f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_OCCLUDED_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step(value: Float): TransitionStep { + return TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + value = value, + transitionState = TransitionState.RUNNING, + ownerName = "LockscreenToOccludedTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt new file mode 100644 index 000000000000..411b1bd04c52 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.table + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class TableLogBufferFactoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val systemClock = FakeSystemClock() + private val underTest = TableLogBufferFactory(dumpManager, systemClock) + + @Test + fun `create - always creates new instance`() { + val b1 = underTest.create(NAME_1, SIZE) + val b1_copy = underTest.create(NAME_1, SIZE) + val b2 = underTest.create(NAME_2, SIZE) + val b2_copy = underTest.create(NAME_2, SIZE) + + assertThat(b1).isNotSameInstanceAs(b1_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b2).isNotSameInstanceAs(b2_copy) + } + + @Test + fun `getOrCreate - reuses instance`() { + val b1 = underTest.getOrCreate(NAME_1, SIZE) + val b1_copy = underTest.getOrCreate(NAME_1, SIZE) + val b2 = underTest.getOrCreate(NAME_2, SIZE) + val b2_copy = underTest.getOrCreate(NAME_2, SIZE) + + assertThat(b1).isSameInstanceAs(b1_copy) + assertThat(b2).isSameInstanceAs(b2_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b1_copy).isNotSameInstanceAs(b2_copy) + } + + companion object { + const val NAME_1 = "name 1" + const val NAME_2 = "name 2" + + const val SIZE = 8 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 4d2d0f05b76a..c0639f34484c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, - InstanceId.fakeInstanceId(-1), -1); + InstanceId.fakeInstanceId(-1), -1, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 52b694fac07c..1687fdc9f76c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.pipeline import android.app.Notification +import android.app.Notification.FLAG_NO_CLEAR import android.app.Notification.MediaStyle import android.app.PendingIntent import android.app.smartspace.SmartspaceAction @@ -228,6 +229,7 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) } @@ -300,6 +302,60 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testLoadMetadata_withExplicitIndicator() { + val metadata = + MediaMetadata.Builder().run { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + build() + } + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadata) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() verify(logger) @@ -603,6 +659,53 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off val desc = @@ -1349,6 +1452,39 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(mediaDataCaptor.value.semanticActions).isNull() } + @Test + fun testNoClearNotOngoing_canDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(false) + it.setFlag(FLAG_NO_CLEAR, true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isTrue() + } + + @Test + fun testOngoing_cannotDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isFalse() + } + /** Helper function to add a media notification and capture the resulting MediaData */ private fun addNotificationAndLoad() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index 6ca34e0bb7ce..e4e95e580a7c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -17,8 +17,10 @@ package com.android.systemui.media.controls.ui import android.app.PendingIntent +import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.MathUtils.abs import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase @@ -30,14 +32,11 @@ import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.PageIndicator import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -55,6 +54,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.floatThat import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -85,7 +85,12 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var debugLogger: MediaCarouselControllerLogger @Mock lateinit var mediaViewController: MediaViewController @Mock lateinit var smartspaceMediaData: SmartspaceMediaData + @Mock lateinit var mediaCarousel: MediaScrollView + @Mock lateinit var pageIndicator: PageIndicator @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + @Captor + lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener> + @Captor lateinit var newConfig: ArgumentCaptor<Configuration> @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> private val clock = FakeSystemClock() @@ -111,6 +116,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { logger, debugLogger ) + verify(configurationController).addCallback(capture(configListener)) verify(mediaDataManager).addListener(capture(listener)) verify(visualStabilityProvider) .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) @@ -642,24 +648,56 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Test fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { val delta = 0.0001F - val paginationSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - val paginationSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) + mediaCarouselController.mediaCarousel = mediaCarousel + mediaCarouselController.pageIndicator = pageIndicator + whenever(mediaCarousel.measuredHeight).thenReturn(100) + whenever(pageIndicator.translationY).thenReturn(80F) + whenever(pageIndicator.height).thenReturn(10) whenever(mediaHostStatesManager.mediaHostStates) .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) whenever(mediaHostState.visible).thenReturn(true) mediaCarouselController.currentEndLocation = LOCATION_QS - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + whenever(mediaHostState.squishFraction).thenReturn(0.938F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta } - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + whenever(mediaHostState.squishFraction).thenReturn(1.0F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta } + } + + @Ignore("b/253229241") + @Test + fun testOnConfigChanged_playersAreAddedBack() { + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + listener.value.onMediaDataLoaded( + "paused local", + null, + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + + val playersSize = MediaPlayerData.players().size + + configListener.value.onConfigChanged(capture(newConfig)) + + assertEquals(playersSize, MediaPlayerData.players().size) + assertEquals( + MediaPlayerData.getMediaPlayerIndex("playing local"), + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index b65f5cb51aaf..b35dd266e422 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.internal.widget.CachingIconView import com.android.systemui.ActivityIntentHelper import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -81,6 +82,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.FakeExecutor @@ -154,6 +156,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var albumView: ImageView private lateinit var titleText: TextView private lateinit var artistText: TextView + private lateinit var explicitIndicator: CachingIconView private lateinit var seamless: ViewGroup private lateinit var seamlessButton: View @Mock private lateinit var seamlessBackground: RippleDrawable @@ -216,14 +219,15 @@ public class MediaControlPanelTest : SysuiTestCase() { this.set(Flags.UMO_SURFACE_RIPPLE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false) this.set(Flags.MEDIA_FALSING_PENALTY, true) + this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true) } @JvmField @Rule val mockito = MockitoJUnit.rule() @Before fun setUp() { - bgExecutor = FakeExecutor(FakeSystemClock()) - mainExecutor = FakeExecutor(FakeSystemClock()) + bgExecutor = FakeExecutor(clock) + mainExecutor = FakeExecutor(clock) whenever(mediaViewController.expandedLayout).thenReturn(expandedSet) whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) @@ -350,6 +354,7 @@ public class MediaControlPanelTest : SysuiTestCase() { appIcon = ImageView(context) titleText = TextView(context) artistText = TextView(context) + explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } seamless = FrameLayout(context) seamless.foreground = seamlessBackground seamlessButton = View(context) @@ -396,6 +401,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) whenever(viewHolder.titleText).thenReturn(titleText) whenever(viewHolder.artistText).thenReturn(artistText) + whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(viewHolder.seamless).thenReturn(seamless) whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) @@ -1019,6 +1025,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { + useRealConstraintSets() player.attachPlayer(viewHolder) player.bindPlayer(mediaData, PACKAGE) @@ -1036,6 +1043,8 @@ public class MediaControlPanelTest : SysuiTestCase() { handler.onAnimationEnd(mockAnimator) assertThat(titleText.getText()).isEqualTo(TITLE) assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) // Rebinding should not trigger animation player.bindPlayer(mediaData, PACKAGE) @@ -1043,6 +1052,36 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun bindTextWithExplicitIndicator() { + useRealConstraintSets() + val mediaDataWitExp = mediaData.copy(isExplicit = true) + player.attachPlayer(viewHolder) + player.bindPlayer(mediaDataWitExp, PACKAGE) + + // Capture animation handler + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator, times(2)).addListener(captor.capture()) + val handler = captor.value + + // Validate text views unchanged but animation started + assertThat(titleText.getText()).isEqualTo("") + assertThat(artistText.getText()).isEqualTo("") + verify(mockAnimator, times(1)).start() + + // Binding only after animator runs + handler.onAnimationEnd(mockAnimator) + assertThat(titleText.getText()).isEqualTo(TITLE) + assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)) + .isEqualTo(ConstraintSet.VISIBLE) + + // Rebinding should not trigger animation + player.bindPlayer(mediaData, PACKAGE) + verify(mockAnimator, times(3)).start() + } + + @Test fun bindTextInterrupted() { val data0 = mediaData.copy(artist = "ARTIST_0") val data1 = mediaData.copy(artist = "ARTIST_1") @@ -2083,6 +2122,27 @@ public class MediaControlPanelTest : SysuiTestCase() { assertThat(player.mRipplesFinishedListener).isNull() } + @Test + fun playTurbulenceNoise_finishesAfterDuration() { + fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true) + fakeFeatureFlag.set(Flags.UMO_TURBULENCE_NOISE, true) + + player.attachPlayer(viewHolder) + + mainExecutor.execute { + player.mRipplesFinishedListener.onRipplesFinish() + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE) + + clock.advanceTime( + MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION + + TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong() + ) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) + } + } + private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 920801f95f5b..a5795184b493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -76,6 +77,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @Mock private lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Captor @@ -110,6 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { keyguardStateController, bypassController, mediaCarouselController, + mediaDataManager, keyguardViewController, dreamOverlayStateController, configurationController, @@ -125,6 +128,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value @@ -357,17 +361,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test - fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() { + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() { goToLockscreen() enterGuidedTransformation() whenever(lockHost.visible).thenReturn(false) whenever(qsHost.visible).thenReturn(true) whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() } @Test + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() { + // To keep the appearing behavior, we need to be in a guided transition + goToLockscreen() + enterGuidedTransformation() + whenever(lockHost.visible).thenReturn(false) + whenever(qsHost.visible).thenReturn(true) + whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) + + assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() + } + + @Test fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 35b0eb678441..4ed6d7cf6bd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -22,13 +22,6 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState @@ -60,9 +53,10 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var controlWidgetState: WidgetState @Mock private lateinit var bgWidgetState: WidgetState @Mock private lateinit var mediaTitleWidgetState: WidgetState + @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState - val delta = 0.0001F + val delta = 0.1F private lateinit var mediaViewController: MediaViewController @@ -76,10 +70,11 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) - player.measureState = TransitionViewState().apply { - this.height = 100 - this.measureHeight = 100 - } + player.measureState = + TransitionViewState().apply { + this.height = 100 + this.measureHeight = 100 + } mediaHostStateHolder.expansion = 1f val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) @@ -128,29 +123,21 @@ class MediaViewControllerTest : SysuiTestCase() { R.id.header_artist to detailWidgetState ) ) - - val detailSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, detailSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(200) + // detail widgets occupy [90, 100] + whenever(detailWidgetState.y).thenReturn(90F) + whenever(detailWidgetState.height).thenReturn(10) + // control widgets occupy [150, 170] + whenever(controlWidgetState.y).thenReturn(150F) + whenever(controlWidgetState.height).thenReturn(20) + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 119F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val detailSquishEnd = - TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, detailSquishEnd) + mediaViewController.squishViewState(mockViewState, 150F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val controlSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, controlSquishMiddle) + mediaViewController.squishViewState(mockViewState, 181.4F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val controlSquishEnd = - TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, controlSquishEnd) + mediaViewController.squishViewState(mockViewState, 200F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } @@ -161,36 +148,33 @@ class MediaViewControllerTest : SysuiTestCase() { .thenReturn( mutableMapOf( R.id.media_title1 to mediaTitleWidgetState, + R.id.media_subtitle1 to mediaSubTitleWidgetState, R.id.media_cover1_container to mediaContainerWidgetState ) ) - - val containerSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(360) + // media container widgets occupy [20, 300] + whenever(mediaContainerWidgetState.y).thenReturn(20F) + whenever(mediaContainerWidgetState.height).thenReturn(280) + // media title widgets occupy [320, 330] + whenever(mediaTitleWidgetState.y).thenReturn(320F) + whenever(mediaTitleWidgetState.height).thenReturn(10) + // media subtitle widgets occupy [340, 350] + whenever(mediaSubTitleWidgetState.y).thenReturn(340F) + whenever(mediaSubTitleWidgetState.height).thenReturn(10) + + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 307.6F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val containerSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishEnd) + mediaViewController.squishViewState(mockViewState, 320F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val titleSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishMiddle) + // media title and media subtitle are in same widget group, should be calculate together and + // have same alpha + mediaViewController.squishViewState(mockViewState, 353.8F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val titleSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishEnd) + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + mediaViewController.squishViewState(mockViewState, 360F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index b16a39f37e39..f5432e22c57e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -102,8 +102,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); - private MediaItem mMediaItem1 = mock(MediaItem.class); - private MediaItem mMediaItem2 = mock(MediaItem.class); private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class); private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class); private MediaMetadata mMediaMetadata = mock(MediaMetadata.class); @@ -127,7 +125,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private LocalMediaManager mLocalMediaManager; private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaDevice> mMediaDevices = new ArrayList<>(); - private List<MediaItem> mMediaItemList = new ArrayList<>(); private List<NearbyDevice> mNearbyDevices = new ArrayList<>(); private MediaDescription mMediaDescription; private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>(); @@ -149,7 +146,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, mKeyguardManager, mFlags); when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); builder.setTitle(TEST_SONG); @@ -160,16 +159,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); - when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1)); - when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2)); - mMediaItemList.add(mMediaItem1); - mMediaItemList.add(mMediaItem2); when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID); - when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); + when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID); - when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); + when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); mNearbyDevices.add(mNearbyDevice1); mNearbyDevices.add(mNearbyDevice2); } @@ -274,8 +269,20 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController.onDevicesUpdated(mNearbyDevices); mMediaOutputController.onDeviceListUpdate(mMediaDevices); - verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE); - verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE); + } + + @Test + public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation() + throws RemoteException { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID); } @Test @@ -292,6 +299,22 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test + public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation() + throws RemoteException { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + verify(mMediaDevice1, never()).setRangeZone(anyInt()); + verify(mMediaDevice2, never()).setRangeZone(anyInt()); + } + + @Test public void advanced_onDeviceListUpdate_verifyDeviceListCallback() { when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); mMediaOutputController.start(mCb); @@ -307,6 +330,35 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(devices.containsAll(mMediaDevices)).isTrue(); assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo( + mMediaDevices.size() + 2); + verify(mCb).onDeviceListChanged(); + } + + @Test + public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() { + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + when(mMediaDevice1.isSuggestedDevice()).thenReturn(true); + when(mMediaDevice2.isSuggestedDevice()).thenReturn(false); + + mMediaOutputController.start(mCb); + reset(mCb); + mMediaOutputController.getMediaItemList().clear(); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + final List<MediaDevice> devices = new ArrayList<>(); + int dividerSize = 0; + for (MediaItem item : mMediaOutputController.getMediaItemList()) { + if (item.getMediaDevice().isPresent()) { + devices.add(item.getMediaDevice().get()); + } + if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { + dividerSize++; + } + } + + assertThat(devices.containsAll(mMediaDevices)).isTrue(); + assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(dividerSize).isEqualTo(2); verify(mCb).onDeviceListChanged(); } 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 4cc12c709fa7..f5b3959b322d 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 @@ -206,6 +206,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + } + + @Test fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, @@ -248,6 +263,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, @@ -934,6 +964,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val BLANK_DEVICE_NAME = " " private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 @@ -942,3 +973,9 @@ private val routeInfo = .addFeature("feature") .setClientPackageName(PACKAGE_NAME) .build() + +private val routeInfoWithBlankDeviceName = + MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) + .addFeature("feature") + .setClientPackageName(PACKAGE_NAME) + .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt index 19d2d334b884..1042ea714936 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -1,12 +1,16 @@ package com.android.systemui.mediaprojection.appselector import android.content.ComponentName +import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.junit.Test @@ -21,11 +25,17 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { private val scope = CoroutineScope(Dispatchers.Unconfined) private val appSelectorComponentName = ComponentName("com.test", "AppSelector") + private val hostUserHandle = UserHandle.of(123) + private val otherUserHandle = UserHandle.of(456) + private val view: MediaProjectionAppSelectorView = mock() + private val featureFlags: FeatureFlags = mock() private val controller = MediaProjectionAppSelectorController( taskListProvider, view, + featureFlags, + hostUserHandle, scope, appSelectorComponentName ) @@ -98,15 +108,72 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) } + @Test + fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesDisabled_bindsOnlyTasksWithHostProfile() { + givenEnterprisePoliciesFeatureFlag(enabled = false) + + val tasks = listOf( + createRecentTask(taskId = 1, userId = hostUserHandle.identifier), + createRecentTask(taskId = 2, userId = otherUserHandle.identifier), + createRecentTask(taskId = 3, userId = hostUserHandle.identifier), + createRecentTask(taskId = 4, userId = otherUserHandle.identifier), + createRecentTask(taskId = 5, userId = hostUserHandle.identifier), + ) + taskListProvider.tasks = tasks + + controller.init() + + verify(view).bind( + listOf( + createRecentTask(taskId = 1, userId = hostUserHandle.identifier), + createRecentTask(taskId = 3, userId = hostUserHandle.identifier), + createRecentTask(taskId = 5, userId = hostUserHandle.identifier), + ) + ) + } + + @Test + fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesEnabled_bindsAllTasks() { + givenEnterprisePoliciesFeatureFlag(enabled = true) + + val tasks = listOf( + createRecentTask(taskId = 1, userId = hostUserHandle.identifier), + createRecentTask(taskId = 2, userId = otherUserHandle.identifier), + createRecentTask(taskId = 3, userId = hostUserHandle.identifier), + createRecentTask(taskId = 4, userId = otherUserHandle.identifier), + createRecentTask(taskId = 5, userId = hostUserHandle.identifier), + ) + taskListProvider.tasks = tasks + + controller.init() + + // TODO(b/233348916) should filter depending on the policies + verify(view).bind( + listOf( + createRecentTask(taskId = 1, userId = hostUserHandle.identifier), + createRecentTask(taskId = 2, userId = otherUserHandle.identifier), + createRecentTask(taskId = 3, userId = hostUserHandle.identifier), + createRecentTask(taskId = 4, userId = otherUserHandle.identifier), + createRecentTask(taskId = 5, userId = hostUserHandle.identifier), + ) + ) + } + + private fun givenEnterprisePoliciesFeatureFlag(enabled: Boolean) { + whenever(featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) + .thenReturn(enabled) + } + private fun createRecentTask( taskId: Int, - topActivityComponent: ComponentName? = null + topActivityComponent: ComponentName? = null, + userId: Int = hostUserHandle.identifier ): RecentTask { return RecentTask( taskId = taskId, topActivityComponent = topActivityComponent, baseIntentComponent = ComponentName("com", "Test"), - userId = 0, + userId = userId, colorBackground = 0 ) } 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 1bc4719c70b7..1a35502ceed6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -31,10 +31,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -69,7 +66,7 @@ public class ColorSchemeTest extends SysuiTestCase { // Expressive applies hue rotations to the theme color. The input theme color has hue // 117, ensuring the hue changed significantly is a strong signal styles are being applied. ColorScheme colorScheme = new ColorScheme(wallpaperColors, false, Style.EXPRESSIVE); - Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().get(6)).getHue(), 0.1); + Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().getS500()).getHue(), 0.1); } @@ -111,7 +108,8 @@ public class ColorSchemeTest extends SysuiTestCase { public void testTertiaryHueWrapsProperly() { int colorInt = 0xffB3588A; // H350 C50 T50 ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */); - int tertiaryMid = colorScheme.getAccent3().get(colorScheme.getAccent3().size() / 2); + int tertiaryMid = colorScheme.getAccent3().getAllShades().get( + colorScheme.getShadeCount() / 2); Cam cam = Cam.fromInt(tertiaryMid); Assert.assertEquals(cam.getHue(), 50.0, 10.0); } @@ -121,7 +119,8 @@ public class ColorSchemeTest extends SysuiTestCase { int colorInt = 0xffB3588A; // H350 C50 T50 ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, Style.SPRITZ /* style */); - int primaryMid = colorScheme.getAccent1().get(colorScheme.getAccent1().size() / 2); + int primaryMid = colorScheme.getAccent1().getAllShades().get( + colorScheme.getShadeCount() / 2); Cam cam = Cam.fromInt(primaryMid); Assert.assertEquals(cam.getChroma(), 12.0, 1.0); } @@ -131,7 +130,8 @@ public class ColorSchemeTest extends SysuiTestCase { int colorInt = 0xffB3588A; // H350 C50 T50 ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, Style.VIBRANT /* style */); - int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + int neutralMid = colorScheme.getNeutral1().getAllShades().get( + colorScheme.getShadeCount() / 2); Cam cam = Cam.fromInt(neutralMid); Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 12.0); } @@ -141,7 +141,8 @@ public class ColorSchemeTest extends SysuiTestCase { int colorInt = 0xffB3588A; // H350 C50 T50 ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, Style.EXPRESSIVE /* style */); - int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + int neutralMid = colorScheme.getNeutral1().getAllShades().get( + colorScheme.getShadeCount() / 2); Cam cam = Cam.fromInt(neutralMid); Assert.assertTrue(cam.getChroma() <= 8.0); } @@ -151,10 +152,11 @@ public class ColorSchemeTest extends SysuiTestCase { 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); + int neutralMid = colorScheme.getNeutral1().getAllShades().get( + colorScheme.getShadeCount() / 2); Assert.assertTrue( Color.red(neutralMid) == Color.green(neutralMid) - && Color.green(neutralMid) == Color.blue(neutralMid) + && Color.green(neutralMid) == Color.blue(neutralMid) ); } @@ -190,15 +192,14 @@ public class ColorSchemeTest extends SysuiTestCase { xml.append(" <").append(styleName).append(">"); List<String> colors = new ArrayList<>(); - for (Stream<Integer> stream: Arrays.asList(colorScheme.getAccent1().stream(), - colorScheme.getAccent2().stream(), - colorScheme.getAccent3().stream(), - colorScheme.getNeutral1().stream(), - colorScheme.getNeutral2().stream())) { + + colorScheme.getAllHues().forEach(schemeHue -> { colors.add("ffffff"); - colors.addAll(stream.map(Integer::toHexString).map(s -> s.substring(2)).collect( - Collectors.toList())); - } + schemeHue.getAllShades().forEach(tone -> { + colors.add(Integer.toHexString(tone).substring(2)); + }); + }); + xml.append(String.join(",", colors)); xml.append("</").append(styleName).append(">\n"); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java index 9bf27a26a682..8b0342eda633 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java @@ -51,6 +51,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.LightBarController; @@ -102,10 +103,10 @@ public class NavigationBarControllerTest extends SysuiTestCase { mock(NavBarHelper.class), mTaskbarDelegate, mNavigationBarFactory, - mock(StatusBarKeyguardViewManager.class), mock(DumpManager.class), mock(AutoHideController.class), mock(LightBarController.class), + TaskStackChangeListeners.getTestInstance(), Optional.of(mock(Pip.class)), Optional.of(mock(BackAnimation.class)), mock(FeatureFlags.class))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 80adbf025e0b..2ad865e6ef11 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -28,6 +28,7 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -44,6 +45,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.hardware.display.DisplayManagerGlobal; @@ -90,6 +92,7 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -203,6 +206,8 @@ public class NavigationBarTest extends SysuiTestCase { private ViewRootImpl mViewRootImpl; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake(); + private TaskStackChangeListeners mTaskStackChangeListeners = + TaskStackChangeListeners.getTestInstance(); @Rule public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); @@ -437,6 +442,14 @@ public class NavigationBarTest extends SysuiTestCase { verify(mNavBarHelper, times(1)).getCurrentSysuiState(); } + @Test + public void testScreenPinningEnabled_updatesSysuiState() { + mNavigationBar.init(); + mTaskStackChangeListeners.getListenerImpl().onLockTaskModeChanged( + ActivityManager.LOCK_TASK_MODE_PINNED); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_SCREEN_PINNING), eq(true)); + } + private NavigationBar createNavBar(Context context) { DeviceProvisionedController deviceProvisionedController = mock(DeviceProvisionedController.class); @@ -481,7 +494,8 @@ public class NavigationBarTest extends SysuiTestCase { mEdgeBackGestureHandler, Optional.of(mock(BackAnimation.class)), mUserContextProvider, - mWakefulnessLifecycle)); + mWakefulnessLifecycle, + mTaskStackChangeListeners)); } private void processAllMessages() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt index 1742c6994246..537dfb821fef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt @@ -1,11 +1,14 @@ package com.android.systemui.navigationbar +import android.app.ActivityManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler import com.android.systemui.recents.OverviewProxyService +import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.shared.system.TaskStackChangeListeners import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.phone.AutoHideController import com.android.systemui.statusbar.phone.LightBarController @@ -14,6 +17,7 @@ import com.android.wm.shell.back.BackAnimation import com.android.wm.shell.pip.Pip import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.any @@ -30,6 +34,7 @@ class TaskbarDelegateTest : SysuiTestCase() { val MODE_GESTURE = 0; val MODE_THREE_BUTTON = 1; + private lateinit var mTaskStackChangeListeners: TaskStackChangeListeners private lateinit var mTaskbarDelegate: TaskbarDelegate @Mock lateinit var mEdgeBackGestureHandlerFactory : EdgeBackGestureHandler.Factory @@ -69,11 +74,12 @@ class TaskbarDelegateTest : SysuiTestCase() { `when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController) `when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState) `when`(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState) + mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance() mTaskbarDelegate = TaskbarDelegate(context, mEdgeBackGestureHandlerFactory, mLightBarControllerFactory) mTaskbarDelegate.setDependencies(mCommandQueue, mOverviewProxyService, mNavBarHelper, mNavigationModeController, mSysUiState, mDumpManager, mAutoHideController, - mLightBarController, mOptionalPip, mBackAnimation) + mLightBarController, mOptionalPip, mBackAnimation, mTaskStackChangeListeners) } @Test @@ -90,4 +96,15 @@ class TaskbarDelegateTest : SysuiTestCase() { mTaskbarDelegate.init(DISPLAY_ID) verify(mEdgeBackGestureHandler, times(1)).onNavigationModeChanged(MODE_GESTURE) } + + @Test + fun screenPinningEnabled_updatesSysuiState() { + mTaskbarDelegate.init(DISPLAY_ID) + mTaskStackChangeListeners.listenerImpl.onLockTaskModeChanged( + ActivityManager.LOCK_TASK_MODE_PINNED) + verify(mSysUiState, times(1)).setFlag( + ArgumentMatchers.eq(QuickStepContract.SYSUI_STATE_SCREEN_PINNING), + ArgumentMatchers.eq(true) + ) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 4a9c7508b1b3..8440455127bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -24,7 +24,7 @@ import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -50,7 +50,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) internal class NoteTaskControllerTest : SysuiTestCase() { - private val notesIntent = Intent(NOTES_ACTION) + private val notesIntent = Intent(ACTION_CREATE_NOTE) @Mock lateinit var context: Context @Mock lateinit var packageManager: PackageManager @@ -93,7 +93,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -102,7 +102,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) - verify(bubbles).showAppBubble(notesIntent) + verify(bubbles).showOrHideAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @@ -113,7 +113,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = true) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -123,7 +123,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -133,7 +133,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -143,7 +143,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -153,7 +153,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -161,7 +161,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -171,7 +171,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 538131a4dd73..010ac5bbb2d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -106,7 +106,9 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { // region handleSystemKey @Test fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { - createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskInitializer() + .callbacks + .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT) verify(noteTaskController).showNoteTask() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt index dd2cc2ffc9db..bbe60f4ba493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -23,11 +23,10 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo -import android.content.pm.ServiceInfo import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -58,19 +57,13 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { } private fun createResolveInfo( - packageName: String = "PackageName", - activityInfo: ActivityInfo? = null, + activityInfo: ActivityInfo? = createActivityInfo(), ): ResolveInfo { - return ResolveInfo().apply { - serviceInfo = - ServiceInfo().apply { - applicationInfo = ApplicationInfo().apply { this.packageName = packageName } - } - this.activityInfo = activityInfo - } + return ResolveInfo().apply { this.activityInfo = activityInfo } } private fun createActivityInfo( + packageName: String = "PackageName", name: String? = "ActivityName", exported: Boolean = true, enabled: Boolean = true, @@ -87,6 +80,7 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { if (turnScreenOn) { flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON } + this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName } } } @@ -107,7 +101,8 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { val actual = resolver.resolveIntent() val expected = - Intent(NOTES_ACTION) + Intent(ACTION_CREATE_NOTE) + .setPackage("PackageName") .setComponent(ComponentName("PackageName", "ActivityName")) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // Compares the string representation of both intents, as they are different instances. @@ -204,7 +199,9 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { @Test fun resolveIntent_packageNameIsBlank_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) } + givenQueryIntentActivities { + listOf(createResolveInfo(createActivityInfo(packageName = ""))) + } val actual = resolver.resolveIntent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt index ca3182affcc1..3281fa9bd8a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile import com.android.systemui.qs.tiles.BluetoothTile import com.android.systemui.qs.tiles.CameraToggleTile import com.android.systemui.qs.tiles.CastTile -import com.android.systemui.qs.tiles.CellularTile import com.android.systemui.qs.tiles.ColorCorrectionTile import com.android.systemui.qs.tiles.ColorInversionTile import com.android.systemui.qs.tiles.DataSaverTile @@ -49,7 +48,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile import com.android.systemui.qs.tiles.RotationLockTile import com.android.systemui.qs.tiles.ScreenRecordTile import com.android.systemui.qs.tiles.UiModeNightTile -import com.android.systemui.qs.tiles.WifiTile import com.android.systemui.qs.tiles.WorkModeTile import com.android.systemui.util.leak.GarbageMonitor import com.google.common.truth.Truth.assertThat @@ -63,10 +61,8 @@ import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever private val specMap = mapOf( - "wifi" to WifiTile::class.java, "internet" to InternetTile::class.java, "bt" to BluetoothTile::class.java, - "cell" to CellularTile::class.java, "dnd" to DndTile::class.java, "inversion" to ColorInversionTile::class.java, "airplane" to AirplaneModeTile::class.java, @@ -102,10 +98,8 @@ class QSFactoryImplTest : SysuiTestCase() { @Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder @Mock private lateinit var customTile: CustomTile - @Mock private lateinit var wifiTile: WifiTile @Mock private lateinit var internetTile: InternetTile @Mock private lateinit var bluetoothTile: BluetoothTile - @Mock private lateinit var cellularTile: CellularTile @Mock private lateinit var dndTile: DndTile @Mock private lateinit var colorInversionTile: ColorInversionTile @Mock private lateinit var airplaneTile: AirplaneModeTile @@ -146,10 +140,8 @@ class QSFactoryImplTest : SysuiTestCase() { factory = QSFactoryImpl( { qsHost }, { customTileBuilder }, - { wifiTile }, { internetTile }, { bluetoothTile }, - { cellularTile }, { dndTile }, { colorInversionTile }, { airplaneTile }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index 08a90b79089e..18e40f633955 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.qs.QSUserSwitcherEvent -import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.source.UserRecord import org.junit.Assert.assertEquals @@ -42,7 +41,6 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -152,15 +150,6 @@ class UserDetailViewAdapterTest : SysuiTestCase() { assertNull(adapter.users.find { it.isManageUsers }) } - @Test - fun clickDismissDialog() { - val shower: UserSwitchDialogController.DialogShower = - mock(UserSwitchDialogController.DialogShower::class.java) - adapter.injectDialogShower(shower) - adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower) - verify(shower).dismiss() - } - private fun createUserRecord(current: Boolean, guest: Boolean) = UserRecord( UserInfo(0 /* id */, "name", 0 /* flags */), diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt index fa1fedb7c119..99c79b0365ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt @@ -39,7 +39,6 @@ import com.android.internal.util.ScreenshotHelper import com.android.internal.util.ScreenshotHelper.ScreenshotRequest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW @@ -101,7 +100,6 @@ class TakeScreenshotServiceTest : SysuiTestCase() { }.`when`(requestProcessor).processAsync(/* request= */ any(), /* callback= */ any()) // Flipped in selected test cases - flags.set(SCREENSHOT_REQUEST_PROCESSOR, false) flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false) service.attach( @@ -149,31 +147,6 @@ class TakeScreenshotServiceTest : SysuiTestCase() { } @Test - fun takeScreenshot_requestProcessorEnabled() { - flags.set(SCREENSHOT_REQUEST_PROCESSOR, true) - - val request = ScreenshotRequest( - TAKE_SCREENSHOT_FULLSCREEN, - SCREENSHOT_KEY_CHORD, - topComponent) - - service.handleRequest(request, { /* onSaved */ }, callback) - - verify(controller, times(1)).takeScreenshotFullscreen( - eq(topComponent), - /* onSavedListener = */ any(), - /* requestCallback = */ any()) - - assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1) - val logEvent = eventLogger.get(0) - - assertEquals("Expected SCREENSHOT_REQUESTED UiEvent", - logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id) - assertEquals("Expected supplied package name", - topComponent.packageName, eventLogger.get(0).packageName) - } - - @Test fun takeScreenshotProvidedImage() { val bounds = Rect(50, 50, 150, 150) val bitmap = makeHardwareBitmap(100, 100) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java new file mode 100644 index 000000000000..bd04b3ccc039 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.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.systemui.screenshot; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.util.FakeSharedPreferences; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class WorkProfileMessageControllerTest { + private static final String DEFAULT_LABEL = "default label"; + private static final String BADGED_DEFAULT_LABEL = "badged default label"; + private static final String APP_LABEL = "app label"; + private static final String BADGED_APP_LABEL = "badged app label"; + private static final UserHandle NON_WORK_USER = UserHandle.of(0); + private static final UserHandle WORK_USER = UserHandle.of(10); + + @Mock + private UserManager mUserManager; + @Mock + private PackageManager mPackageManager; + @Mock + private Context mContext; + @Mock + private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay; + @Mock + private Drawable mActivityIcon; + @Mock + private Drawable mBadgedActivityIcon; + @Mock + private ActivityInfo mActivityInfo; + @Captor + private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; + + private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences(); + + private WorkProfileMessageController mMessageController; + + @Before + public void setup() throws PackageManager.NameNotFoundException { + MockitoAnnotations.initMocks(this); + + when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true); + when(mContext.getSharedPreferences( + eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME), + eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences); + when(mContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL); + when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any())) + .thenReturn(BADGED_DEFAULT_LABEL); + when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any())) + .thenReturn(BADGED_APP_LABEL); + when(mPackageManager.getActivityIcon(any(ComponentName.class))) + .thenReturn(mActivityIcon); + when(mPackageManager.getUserBadgedIcon( + any(), any())).thenReturn(mBadgedActivityIcon); + when(mPackageManager.getActivityInfo(any(), + any(PackageManager.ComponentInfoFlags.class))).thenReturn(mActivityInfo); + when(mActivityInfo.loadLabel(eq(mPackageManager))).thenReturn(APP_LABEL); + + mSharedPreferences.edit().putBoolean( + WorkProfileMessageController.PREFERENCE_KEY, false).apply(); + + mMessageController = new WorkProfileMessageController(mContext, mUserManager, + mPackageManager); + } + + @Test + public void testOnScreenshotTaken_notManaged() { + mMessageController.onScreenshotTaken(NON_WORK_USER, mMessageDisplay); + + verify(mMessageDisplay, never()) + .showWorkProfileMessage(any(), nullable(Drawable.class), any()); + } + + @Test + public void testOnScreenshotTaken_alreadyDismissed() { + mSharedPreferences.edit().putBoolean( + WorkProfileMessageController.PREFERENCE_KEY, true).apply(); + + mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); + + verify(mMessageDisplay, never()) + .showWorkProfileMessage(any(), nullable(Drawable.class), any()); + } + + @Test + public void testOnScreenshotTaken_packageNotFound() + throws PackageManager.NameNotFoundException { + when(mPackageManager.getActivityInfo(any(), + any(PackageManager.ComponentInfoFlags.class))).thenThrow( + new PackageManager.NameNotFoundException()); + + mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); + + verify(mMessageDisplay).showWorkProfileMessage( + eq(BADGED_DEFAULT_LABEL), eq(null), any()); + } + + @Test + public void testOnScreenshotTaken() { + mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); + + verify(mMessageDisplay).showWorkProfileMessage( + eq(BADGED_APP_LABEL), eq(mBadgedActivityIcon), mRunnableArgumentCaptor.capture()); + + // Dismiss hasn't been tapped, preference untouched. + assertFalse( + mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false)); + + mRunnableArgumentCaptor.getValue().run(); + + // After dismiss has been tapped, the setting should be updated. + assertTrue( + mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false)); + } +} + diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt new file mode 100644 index 000000000000..3710281499b3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt @@ -0,0 +1,100 @@ +package com.android.systemui.settings + +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.Handler +import android.os.UserHandle +import android.os.UserManager +import androidx.concurrent.futures.DirectExecutor +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Parameterized::class) +class UserTrackerImplReceiveTest : SysuiTestCase() { + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Iterable<String> = + listOf( + Intent.ACTION_USER_INFO_CHANGED, + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, + Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, + Intent.ACTION_MANAGED_PROFILE_REMOVED, + Intent.ACTION_MANAGED_PROFILE_UNLOCKED + ) + } + + private val executor: Executor = DirectExecutor.INSTANCE + + @Mock private lateinit var context: Context + @Mock private lateinit var userManager: UserManager + @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager + @Mock(stubOnly = true) private lateinit var handler: Handler + + @Parameterized.Parameter lateinit var intentAction: String + @Mock private lateinit var callback: UserTracker.Callback + @Captor private lateinit var captor: ArgumentCaptor<List<UserInfo>> + + private lateinit var tracker: UserTrackerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + `when`(context.user).thenReturn(UserHandle.SYSTEM) + `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context) + + tracker = UserTrackerImpl(context, userManager, dumpManager, handler) + } + + @Test + fun `calls callback and updates profiles when an intent received`() { + tracker.initialize(0) + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + tracker.onReceive(context, Intent(intentAction)) + + verify(callback, times(0)).onUserChanged(anyInt(), any()) + verify(callback, times(1)).onProfilesChanged(capture(captor)) + assertThat(captor.value.map { it.id }).containsExactly(0, profileID) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 52462c7186d4..e65bbb1bea08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -124,6 +124,16 @@ class UserTrackerImplTest : SysuiTestCase() { verify(context).registerReceiverForAllUsers( eq(tracker), capture(captor), isNull(), eq(handler)) + with(captor.value) { + assertThat(countActions()).isEqualTo(7) + assertThat(hasAction(Intent.ACTION_USER_SWITCHED)).isTrue() + assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() + } } @Test @@ -280,37 +290,6 @@ class UserTrackerImplTest : SysuiTestCase() { } @Test - fun testCallbackCalledOnProfileChanged() { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intent) - - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) - } - - @Test fun testCallbackCalledOnUserInfoChanged() { tracker.initialize(0) val callback = TestCallback() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt index 88651c1292c3..ed9baf5b1c9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner +import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START @@ -92,12 +93,12 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f) assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID) - assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f) + assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd) .isEqualTo(PARENT_ID) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias) - .isEqualTo(1f) + .isEqualTo(0.5f) assertThat(getConstraint(R.id.privacy_container).layout.endToEnd) .isEqualTo(R.id.end_guide) @@ -108,11 +109,12 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { @Test fun testEdgeElementsAlignedWithEdge_largeScreen() { with(largeScreenConstraint) { - assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID) - assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f) + assertThat(getConstraint(R.id.clock).layout.startToEnd).isEqualTo(R.id.begin_guide) + assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0.5f) - assertThat(getConstraint(R.id.privacy_container).layout.endToEnd).isEqualTo(PARENT_ID) - assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f) + assertThat(getConstraint(R.id.privacy_container).layout.endToStart) + .isEqualTo(R.id.end_guide) + assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(0.5f) } } @@ -218,7 +220,12 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { .isEqualTo(cutoutEnd - padding) } - assertThat(changes.largeScreenConstraintsChanges).isNull() + with(largeScreenConstraint) { + assertThat(getConstraint(R.id.begin_guide).layout.guideBegin) + .isEqualTo(cutoutStart - padding) + assertThat(getConstraint(R.id.end_guide).layout.guideEnd) + .isEqualTo(cutoutEnd - padding) + } } @Test @@ -245,7 +252,10 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0) } - assertThat(changes.largeScreenConstraintsChanges).isNull() + with(largeScreenConstraint) { + assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0) + assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0) + } } @Test @@ -331,10 +341,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { val views = mapOf( R.id.clock to "clock", R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> assertWithMessage("$name has 0 height in qqs") @@ -352,11 +360,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() { val views = mapOf( R.id.clock to "clock", - R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> expect.withMessage("$name changes height") @@ -369,8 +374,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { } private fun Int.fromConstraint() = when (this) { - -1 -> "MATCH_PARENT" - -2 -> "WRAP_CONTENT" + ViewGroup.LayoutParams.MATCH_PARENT -> "MATCH_PARENT" + ViewGroup.LayoutParams.WRAP_CONTENT -> "WRAP_CONTENT" else -> toString() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index 1d30ad9293a0..f580f5e00f67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -182,6 +182,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + whenever(view.alpha).thenReturn(1f) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt index b4c8f981b760..b568122d3fed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.shade +import android.animation.ValueAnimator import android.app.StatusBarManager import android.content.Context import android.testing.AndroidTestingRunner @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.policy.VariableDateViewController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -37,6 +39,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.mock @@ -75,6 +78,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE + var viewAlpha = 1f private lateinit var mLargeScreenShadeHeaderController: LargeScreenShadeHeaderController private lateinit var carrierIconSlots: List<String> @@ -101,6 +105,13 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + + whenever(view.setAlpha(anyFloat())).then { + viewAlpha = it.arguments[0] as Float + null + } + whenever(view.alpha).thenAnswer { _ -> viewAlpha } + whenever(variableDateViewControllerFactory.create(any())) .thenReturn(variableDateViewController) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) @@ -155,6 +166,16 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun alphaChangesUpdateVisibility() { + makeShadeVisible() + mLargeScreenShadeHeaderController.shadeExpandedFraction = 0f + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + + mLargeScreenShadeHeaderController.shadeExpandedFraction = 1f + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + } + + @Test fun singleCarrier_enablesCarrierIconsInStatusIcons() { whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true) @@ -239,6 +260,39 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun testShadeExpanded_true_alpha_zero_invisible() { + view.alpha = 0f + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test + fun animatorCallsUpdateVisibilityOnUpdate() { + val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) + whenever(view.animate()).thenReturn(animator) + + mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, 0L) + + val updateCaptor = argumentCaptor<ValueAnimator.AnimatorUpdateListener>() + verify(animator).setUpdateListener(capture(updateCaptor)) + + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + view.alpha = 1f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + + view.alpha = 0f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test fun demoMode_attachDemoMode() { val cb = argumentCaptor<DemoMode>() verify(demoModeController).addCallback(capture(cb)) 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 65b2ac0eb2d3..0f3d4a8ca59a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -103,10 +103,14 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; @@ -293,8 +297,13 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; + @Mock private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; + @Mock private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel; + @Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; + @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Mock private CoroutineDispatcher mMainDispatcher; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private MotionEvent mDownMotionEvent; @Captor private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> @@ -511,8 +520,12 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { systemClock, mKeyguardBottomAreaViewModel, mKeyguardBottomAreaInteractor, + mAlternateBouncerInteractor, mDreamingToLockscreenTransitionViewModel, mOccludedToLockscreenTransitionViewModel, + mLockscreenToDreamingTransitionViewModel, + mGoneToDreamingTransitionViewModel, + mLockscreenToOccludedTransitionViewModel, mMainDispatcher, mKeyguardTransitionInteractor, mDumpManager); 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 08a9c9664ae0..526dc8d150fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -46,11 +46,14 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -68,6 +71,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -91,13 +96,21 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Mock private ShadeWindowLogger mShadeWindowLogger; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; + @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; - + private float mPreferredRefreshRate = -1; @Before public void setUp() { MockitoAnnotations.initMocks(this); + // Preferred refresh rate is equal to the first displayMode's refresh rate + mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate(); + overrideResource( + R.integer.config_keyguardRefreshRate, + (int) mPreferredRefreshRate + ); + when(mDozeParameters.getAlwaysOn()).thenReturn(true); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); @@ -117,6 +130,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mNotificationShadeWindowController.attach(); verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any()); + verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt()); } @Test @@ -334,4 +348,59 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { assertThat(mLayoutParameters.getValue().screenOrientation) .isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); } + + @Test + public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() { + // GIVEN udfps is enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); + + // WHEN keyguard is showing + setKeyguardShowing(); + + // THEN min and max refresh rate is set to the preferredRefreshRate + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate); + } + + @Test + public void udfpsNotEnrolled_refreshRateUnset() { + // GIVEN udfps is NOT enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + + // WHEN keyguard is showing + setKeyguardShowing(); + + // THEN min and max refresh rate aren't set (set to 0) + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0); + } + + @Test + public void keyguardNotShowing_refreshRateUnset() { + // GIVEN UDFPS is enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); + + // WHEN keyguard is NOT showing + mNotificationShadeWindowController.setKeyguardShowing(false); + + // THEN min and max refresh rate aren't set (set to 0) + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0); + } + + private void setKeyguardShowing() { + mNotificationShadeWindowController.setKeyguardShowing(true); + mNotificationShadeWindowController.setKeyguardGoingAway(false); + mNotificationShadeWindowController.setKeyguardFadingAway(false); + mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index c3207c2f58a5..4c768253202a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -30,6 +30,8 @@ import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.dock.DockManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -97,10 +99,13 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { private lateinit var pulsingGestureListener: PulsingGestureListener @Mock private lateinit var notificationInsetsController: NotificationInsetsController + @Mock + private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerContainer: ViewGroup @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @Mock lateinit var keyguardHostViewController: KeyguardHostViewController + @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> private lateinit var interactionEventHandler: InteractionEventHandler @@ -132,7 +137,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { pulsingGestureListener, featureFlags, keyguardBouncerViewModel, - keyguardBouncerComponentFactory + keyguardBouncerComponentFactory, + alternateBouncerInteractor, + keyguardTransitionInteractor, ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index 4bf00c4ccb51..d43562443d6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -40,6 +40,8 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; @@ -93,6 +95,8 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel; @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory; @Mock private NotificationInsetsController mNotificationInsetsController; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; + @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler> mInteractionEventHandlerCaptor; @@ -132,7 +136,9 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mPulsingGestureListener, mFeatureFlags, mKeyguardBouncerViewModel, - mKeyguardBouncerComponentFactory + mKeyguardBouncerComponentFactory, + mAlternateBouncerInteractor, + mKeyguardTransitionInteractor ); mController.setupExpandedStatusBar(); mController.setDragDownHelper(mDragDownHelper); @@ -155,7 +161,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should intercept touch @@ -168,7 +174,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(false); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we shouldn't intercept touch @@ -181,7 +187,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should handle the touch diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt new file mode 100644 index 000000000000..5fb1e79e93e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartspace + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.smartspace.config.BcSmartspaceConfigProvider +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BcSmartspaceConfigProviderTest : SysuiTestCase() { + @Mock private lateinit var featureFlags: FeatureFlags + + private lateinit var configProvider: BcSmartspaceConfigProvider + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + configProvider = BcSmartspaceConfigProvider(featureFlags) + } + + @Test + fun isDefaultDateWeatherDisabled_flagIsTrue_returnsTrue() { + whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true) + + assertTrue(configProvider.isDefaultDateWeatherDisabled) + } + + @Test + fun isDefaultDateWeatherDisabled_flagIsFalse_returnsFalse() { + whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false) + + assertFalse(configProvider.isDefaultDateWeatherDisabled) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index 001e1f4d8086..c5432c5fe330 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -27,6 +27,7 @@ import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dreams.smartspace.DreamSmartspaceController +import com.android.systemui.plugins.BcSmartspaceConfigPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView import com.android.systemui.plugins.FalsingManager @@ -94,6 +95,8 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { private class TestView(context: Context?) : View(context), SmartspaceView { override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} + override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {} + override fun setPrimaryTextColor(color: Int) {} override fun setIsDreaming(isDreaming: Boolean) {} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index d2dd43308fcc..610bb13c6016 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -99,6 +99,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -177,6 +178,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Mock private FaceHelpMessageDeferral mFaceHelpMessageDeferral; @Mock + private AlternateBouncerInteractor mAlternateBouncerInteractor; + @Mock private ScreenLifecycle mScreenLifecycle; @Mock private AuthController mAuthController; @@ -273,7 +276,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mUserManager, mExecutor, mExecutor, mFalsingManager, mAuthController, mLockPatternUtils, mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager, - mFaceHelpMessageDeferral, mock(KeyguardLogger.class)); + mFaceHelpMessageDeferral, mock(KeyguardLogger.class), + mAlternateBouncerInteractor); mController.init(); mController.setIndicationArea(mIndicationArea); verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 3d11ced6207d..702f278746be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -244,6 +244,14 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { } @Test + fun testGoToLockedShadeAlwaysCreatesQSAnimationInSplitShade() { + enableSplitShade() + transitionController.goToLockedShade(null, needsQSAnimation = true) + verify(notificationPanelController).animateToFullShade(anyLong()) + assertNotNull(transitionController.dragDownAnimator) + } + + @Test fun testDragDownAmountDoesntCallOutInLockedDownShade() { whenever(nsslController.isInLockedDownShade).thenReturn(true) transitionController.dragDownAmount = 10f diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt index ddcf59ec001f..4bcb54ddbbc0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceConfigPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView @@ -115,6 +116,9 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { private lateinit var plugin: BcSmartspaceDataPlugin @Mock + private lateinit var configPlugin: BcSmartspaceConfigPlugin + + @Mock private lateinit var controllerListener: SmartspaceTargetListener @Captor @@ -209,7 +213,8 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { executor, bgExecutor, handler, - Optional.of(plugin) + Optional.of(plugin), + Optional.of(configPlugin), ) verify(deviceProvisionedController).addCallback(capture(deviceProvisionedCaptor)) @@ -520,6 +525,7 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { verify(smartspaceManager, never()).createSmartspaceSession(any()) verify(smartspaceView2).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD) verify(smartspaceView2).registerDataProvider(plugin) + verify(smartspaceView2).registerConfigProvider(configPlugin) } @Test @@ -557,6 +563,7 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { verify(smartspaceView).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD) verify(smartspaceView).registerDataProvider(plugin) + verify(smartspaceView).registerConfigProvider(configPlugin) verify(smartspaceSession) .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor)) sessionListener = sessionListenerCaptor.value @@ -638,6 +645,9 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) { } + override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) { + } + override fun setPrimaryTextColor(color: Int) { } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 09f8a10f88c7..a8690388c3e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -137,7 +136,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); allowTestableLooperAsMainThread(); - when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); mListBuilder = new ShadeListBuilder( mDumpManager, @@ -1998,29 +1996,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test - public void testActiveOrdering_withLegacyStability() { - when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); - assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change - assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X - assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap - } - - @Test - public void testStableOrdering_withLegacyStability() { - when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); - mStabilityManager.setAllowEntryReordering(false); - assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change - assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X - assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap - } - - @Test public void testStableOrdering() { - when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); mStabilityManager.setAllowEntryReordering(false); // No input or output assertOrder("", "", "", true); @@ -2076,7 +2052,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { @Test public void testActiveOrdering() { - when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X @@ -2133,7 +2108,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { @Test public void stableOrderingDisregardedWithSectionChange() { - when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); // GIVEN the first sectioner's packages can be changed from run-to-run List<String> mutableSectionerPackages = new ArrayList<>(); mutableSectionerPackages.add(PACKAGE_1); @@ -2229,49 +2203,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test - public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() { - when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false); - - // GIVEN a notification group is on screen - mStabilityManager.setAllowEntryReordering(false); - - // WHEN the list is originally built with reordering disabled (and section changes allowed) - addNotif(0, PACKAGE_1).setRank(2); - addNotif(1, PACKAGE_1).setRank(3); - addGroupSummary(2, PACKAGE_1, "group").setRank(4); - addGroupChild(3, PACKAGE_1, "group").setRank(5); - addGroupChild(4, PACKAGE_1, "group").setRank(6); - dispatchBuild(); - - verifyBuiltList( - notif(0), - notif(1), - group( - summary(2), - child(3), - child(4) - ) - ); - - // WHEN the notification summary rank increases and children removed - setNewRank(notif(2).entry, 1); - mEntrySet.remove(4); - mEntrySet.remove(3); - dispatchBuild(); - - // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked, - // despite visual stability being active - verifyBuiltList( - notif(2), - notif(0), - notif(1) - ); - } - - @Test public void groupRevertingToSummaryRetainsStablePosition() { - when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); - // GIVEN a notification group is on screen mStabilityManager.setAllowEntryReordering(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt index 8275c0c24339..9b3626bfc9ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt @@ -127,6 +127,6 @@ class TargetSdkResolverTest : SysuiTestCase() { NotificationManager.IMPORTANCE_DEFAULT, null, null, null, null, null, true, 0, false, -1, false, null, null, false, false, - false, null, 0, false) + false, null, 0, false, 0) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 9d531a165f1f..4559a23a4f5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -44,16 +44,23 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; import android.graphics.Color; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.Drawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.util.DisplayMetrics; import android.view.View; +import android.widget.ImageView; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.widget.CachingIconView; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; @@ -61,6 +68,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; import org.junit.Assert; @@ -72,6 +80,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Arrays; import java.util.List; @SmallTest @@ -96,6 +105,9 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL); + FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags(); + fakeFeatureFlags.set(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE, true); + mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags); // create a standard private notification row Notification normalNotif = mNotificationTestHelper.createNotification(); normalNotif.publicVersion = null; @@ -559,4 +571,123 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f); Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f); } + + @Test + public void testSetContentAnimationRunning_Run() throws Exception { + // Create views for the notification row. + NotificationContentView publicLayout = mock(NotificationContentView.class); + mNotifRow.setPublicLayout(publicLayout); + NotificationContentView privateLayout = mock(NotificationContentView.class); + mNotifRow.setPrivateLayout(privateLayout); + + mNotifRow.setAnimationRunning(true); + verify(publicLayout, times(1)).setContentAnimationRunning(true); + verify(privateLayout, times(1)).setContentAnimationRunning(true); + } + + @Test + public void testSetContentAnimationRunning_Stop() { + // Create views for the notification row. + NotificationContentView publicLayout = mock(NotificationContentView.class); + mNotifRow.setPublicLayout(publicLayout); + NotificationContentView privateLayout = mock(NotificationContentView.class); + mNotifRow.setPrivateLayout(privateLayout); + + mNotifRow.setAnimationRunning(false); + verify(publicLayout, times(1)).setContentAnimationRunning(false); + verify(privateLayout, times(1)).setContentAnimationRunning(false); + } + + @Test + public void testSetContentAnimationRunningInGroupChild_Run() { + // Creates parent views on mGroupRow. + NotificationContentView publicParentLayout = mock(NotificationContentView.class); + mGroupRow.setPublicLayout(publicParentLayout); + NotificationContentView privateParentLayout = mock(NotificationContentView.class); + mGroupRow.setPrivateLayout(privateParentLayout); + + // Create child views on mNotifRow. + NotificationContentView publicChildLayout = mock(NotificationContentView.class); + mNotifRow.setPublicLayout(publicChildLayout); + NotificationContentView privateChildLayout = mock(NotificationContentView.class); + mNotifRow.setPrivateLayout(privateChildLayout); + when(mNotifRow.isGroupExpanded()).thenReturn(true); + setMockChildrenContainer(mGroupRow, mNotifRow); + + mGroupRow.setAnimationRunning(true); + verify(publicParentLayout, times(1)).setContentAnimationRunning(true); + verify(privateParentLayout, times(1)).setContentAnimationRunning(true); + // The child layouts should be started too. + verify(publicChildLayout, times(1)).setContentAnimationRunning(true); + verify(privateChildLayout, times(1)).setContentAnimationRunning(true); + } + + + @Test + public void testSetIconAnimationRunningGroup_Run() { + // Create views for a group row. + NotificationContentView publicParentLayout = mock(NotificationContentView.class); + mGroupRow.setPublicLayout(publicParentLayout); + NotificationContentView privateParentLayout = mock(NotificationContentView.class); + mGroupRow.setPrivateLayout(privateParentLayout); + when(mGroupRow.isGroupExpanded()).thenReturn(true); + + // Sets up mNotifRow as a child ExpandableNotificationRow. + NotificationContentView publicChildLayout = mock(NotificationContentView.class); + mNotifRow.setPublicLayout(publicChildLayout); + NotificationContentView privateChildLayout = mock(NotificationContentView.class); + mNotifRow.setPrivateLayout(privateChildLayout); + when(mNotifRow.isGroupExpanded()).thenReturn(true); + + NotificationChildrenContainer mockContainer = + setMockChildrenContainer(mGroupRow, mNotifRow); + + // Mock the children view wrappers, and give them each an icon. + NotificationViewWrapper mockViewWrapper = mock(NotificationViewWrapper.class); + when(mockContainer.getNotificationViewWrapper()).thenReturn(mockViewWrapper); + CachingIconView mockIcon = mock(CachingIconView.class); + when(mockViewWrapper.getIcon()).thenReturn(mockIcon); + + NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class); + when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper); + CachingIconView mockLowPriorityIcon = mock(CachingIconView.class); + when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon); + + // Give the icon image views drawables, so we can make sure they animate. + // We use both AnimationDrawables and AnimatedVectorDrawables to ensure both work. + AnimationDrawable drawable = mock(AnimationDrawable.class); + AnimatedVectorDrawable vectorDrawable = mock(AnimatedVectorDrawable.class); + setDrawableIconsInImageView(mockIcon, drawable, vectorDrawable); + + AnimationDrawable lowPriDrawable = mock(AnimationDrawable.class); + AnimatedVectorDrawable lowPriVectorDrawable = mock(AnimatedVectorDrawable.class); + setDrawableIconsInImageView(mockLowPriorityIcon, lowPriDrawable, lowPriVectorDrawable); + + mGroupRow.setAnimationRunning(true); + verify(drawable, times(1)).start(); + verify(vectorDrawable, times(1)).start(); + verify(lowPriDrawable, times(1)).start(); + verify(lowPriVectorDrawable, times(1)).start(); + } + + private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable, + Drawable rightIconDrawable) { + ImageView iconView = mock(ImageView.class); + when(icon.findViewById(com.android.internal.R.id.icon)).thenReturn(iconView); + when(iconView.getDrawable()).thenReturn(iconDrawable); + + ImageView rightIconView = mock(ImageView.class); + when(icon.findViewById(com.android.internal.R.id.right_icon)).thenReturn(rightIconView); + when(rightIconView.getDrawable()).thenReturn(rightIconDrawable); + } + + private NotificationChildrenContainer setMockChildrenContainer( + ExpandableNotificationRow parentRow, ExpandableNotificationRow childRow) { + List<ExpandableNotificationRow> rowList = Arrays.asList(childRow); + NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class); + when(mockContainer.getNotificationChildCount()).thenReturn(1); + when(mockContainer.getAttachedChildren()).thenReturn(rowList); + parentRow.setChildrenContainer(mockContainer); + return mockContainer; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java index 1f92b0a42061..819a75bffc8e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; @@ -98,5 +100,16 @@ public class FooterViewTest extends SysuiTestCase { mView.setSecondaryVisible(true /* visible */, true /* animate */); } + + @Test + public void testSetFooterLabelTextAndIcon() { + mView.setFooterLabelTextAndIcon( + R.string.unlock_to_see_notif_text, + R.drawable.ic_friction_lock_closed); + assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.GONE); + assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.GONE); + assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility()) + .isEqualTo(View.VISIBLE); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 562b4dfb35ef..7b2051da4d15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -34,9 +34,12 @@ import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.statusbar.notification.FeedbackIcon import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -44,6 +47,7 @@ import org.mockito.Mock import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations.initMocks @@ -305,6 +309,86 @@ class NotificationContentViewTest : SysuiTestCase() { assertEquals(0, getMarginBottom(actionListMarginTarget)) } + @Test + fun onSetAnimationRunning() { + // Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set + val mockContracted = mock<NotificationViewWrapper>() + val mockExpanded = mock<NotificationViewWrapper>() + val mockHeadsUp = mock<NotificationViewWrapper>() + + view.setContractedWrapper(mockContracted) + view.setExpandedWrapper(mockExpanded) + view.setHeadsUpWrapper(mockHeadsUp) + + // When: we set content animation running. + assertTrue(view.setContentAnimationRunning(true)) + + // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning + // called on them. + verify(mockContracted, times(1)).setAnimationsRunning(true) + verify(mockExpanded, times(1)).setAnimationsRunning(true) + verify(mockHeadsUp, times(1)).setAnimationsRunning(true) + + // When: we set content animation running true _again_. + assertFalse(view.setContentAnimationRunning(true)) + + // Then: the children should not have setAnimationRunning called on them again. + // Verify counts number of calls so far on the object, so these still register as 1. + verify(mockContracted, times(1)).setAnimationsRunning(true) + verify(mockExpanded, times(1)).setAnimationsRunning(true) + verify(mockHeadsUp, times(1)).setAnimationsRunning(true) + } + + @Test + fun onSetAnimationStopped() { + // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set + val mockContracted = mock<NotificationViewWrapper>() + val mockExpanded = mock<NotificationViewWrapper>() + val mockHeadsUp = mock<NotificationViewWrapper>() + + view.setContractedWrapper(mockContracted) + view.setExpandedWrapper(mockExpanded) + view.setHeadsUpWrapper(mockHeadsUp) + + // When: we set content animation running. + assertTrue(view.setContentAnimationRunning(true)) + + // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning + // called on them. + verify(mockContracted).setAnimationsRunning(true) + verify(mockExpanded).setAnimationsRunning(true) + verify(mockHeadsUp).setAnimationsRunning(true) + + // When: we set content animation running false, the state changes, so the function + // returns true. + assertTrue(view.setContentAnimationRunning(false)) + + // Then: the children have their animations stopped. + verify(mockContracted).setAnimationsRunning(false) + verify(mockExpanded).setAnimationsRunning(false) + verify(mockHeadsUp).setAnimationsRunning(false) + } + + @Test + fun onSetAnimationInitStopped() { + // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set + val mockContracted = mock<NotificationViewWrapper>() + val mockExpanded = mock<NotificationViewWrapper>() + val mockHeadsUp = mock<NotificationViewWrapper>() + + view.setContractedWrapper(mockContracted) + view.setExpandedWrapper(mockExpanded) + view.setHeadsUpWrapper(mockHeadsUp) + + // When: we try to stop the animations before they've been started. + assertFalse(view.setContentAnimationRunning(false)) + + // Then: the children should not have setAnimationRunning called on them again. + verify(mockContracted, never()).setAnimationsRunning(false) + verify(mockExpanded, never()).setAnimationsRunning(false) + verify(mockHeadsUp, never()).setAnimationsRunning(false) + } + private fun createMockContainingNotification(notificationEntry: NotificationEntry) = mock<ExpandableNotificationRow>().apply { whenever(this.entry).thenReturn(notificationEntry) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 728e0265c729..e4fc4d5d54ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -53,6 +53,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.TestableDependency; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -130,6 +131,7 @@ public class NotificationTestHelper { public final OnUserInteractionCallback mOnUserInteractionCallback; public final Runnable mFutureDismissalRunnable; private @InflationFlag int mDefaultInflationFlags; + private FeatureFlags mFeatureFlags; public NotificationTestHelper( Context context, @@ -193,12 +195,17 @@ public class NotificationTestHelper { mFutureDismissalRunnable = mock(Runnable.class); when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt())) .thenReturn(mFutureDismissalRunnable); + mFeatureFlags = mock(FeatureFlags.class); } public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) { mDefaultInflationFlags = defaultInflationFlags; } + public void setFeatureFlags(FeatureFlags featureFlags) { + mFeatureFlags = featureFlags; + } + public ExpandableNotificationRowLogger getMockLogger() { return mMockLogger; } @@ -561,7 +568,8 @@ public class NotificationTestHelper { mock(NotificationGutsManager.class), mock(MetricsLogger.class), mock(SmartReplyConstants.class), - mock(SmartReplyController.class)); + mock(SmartReplyController.class), + mFeatureFlags); row.setAboveShelfChangedListener(aboveShelf -> { }); mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java index 509ba4194e5e..8f88501a38f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.notification.row.wrapper; +import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; import android.app.Notification; -import android.content.Context; +import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.testing.AndroidTestingRunner; @@ -28,11 +29,11 @@ import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.view.LayoutInflater; import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.test.filters.SmallTest; +import com.android.internal.R; +import com.android.internal.widget.BigPictureNotificationImageView; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -73,4 +74,38 @@ public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase Notification.EXTRA_LARGE_ICON_BIG, new Bundle()); wrapper.onContentUpdated(mRow); } + + @Test + public void setAnimationsRunning_Run() { + BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture); + AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class); + + assertNotNull(imageView); + imageView.setImageDrawable(mockDrawable); + + NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext, + mView, mRow); + // Required to re-initialize the imageView to the imageView created above. + wrapper.onContentUpdated(mRow); + + wrapper.setAnimationsRunning(true); + verify(mockDrawable).start(); + } + + @Test + public void setAnimationsRunning_Stop() { + BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture); + AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class); + + assertNotNull(imageView); + imageView.setImageDrawable(mockDrawable); + + NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext, + mView, mRow); + // Required to re-initialize the imageView to the imageView created above. + wrapper.onContentUpdated(mRow); + + wrapper.setAnimationsRunning(false); + verify(mockDrawable).stop(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt new file mode 100644 index 000000000000..3fa68bb69da2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt @@ -0,0 +1,143 @@ +/* + * 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.notification.row.wrapper + +import android.graphics.drawable.AnimatedImageDrawable +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.internal.widget.CachingIconView +import com.android.internal.widget.ConversationLayout +import com.android.internal.widget.MessagingGroup +import com.android.internal.widget.MessagingImageMessage +import com.android.internal.widget.MessagingLinearLayout +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { + + private lateinit var mRow: ExpandableNotificationRow + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + mRow = helper.createRow() + } + + @Test + fun setAnimationsRunning_Run() { + // Creates a mocked out NotificationEntry of ConversationLayout type, + // with a mock imageMessage.drawable embedded in its MessagingImageMessages + // (both top level, and in a group). + val mockDrawable = mock<AnimatedImageDrawable>() + val mockDrawable2 = mock<AnimatedImageDrawable>() + val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2) + + val wrapper: NotificationViewWrapper = + NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow) + wrapper.onContentUpdated(mRow) + wrapper.setAnimationsRunning(true) + + // Verifies that each AnimatedImageDrawable is started animating. + verify(mockDrawable).start() + verify(mockDrawable2).start() + } + + @Test + fun setAnimationsRunning_Stop() { + // Creates a mocked out NotificationEntry of ConversationLayout type, + // with a mock imageMessage.drawable embedded in its MessagingImageMessages + // (both top level, and in a group). + val mockDrawable = mock<AnimatedImageDrawable>() + val mockDrawable2 = mock<AnimatedImageDrawable>() + val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2) + + val wrapper: NotificationViewWrapper = + NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow) + wrapper.onContentUpdated(mRow) + wrapper.setAnimationsRunning(false) + + // Verifies that each AnimatedImageDrawable is started animating. + verify(mockDrawable).stop() + verify(mockDrawable2).stop() + } + + private fun fakeConversationLayout( + mockDrawableGroupMessage: AnimatedImageDrawable, + mockDrawableImageMessage: AnimatedImageDrawable + ): View { + val mockMessagingImageMessage: MessagingImageMessage = + mock<MessagingImageMessage>().apply { + whenever(drawable).thenReturn(mockDrawableImageMessage) + } + val mockImageMessageContainer: MessagingLinearLayout = + mock<MessagingLinearLayout>().apply { + whenever(childCount).thenReturn(1) + whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage) + } + + val mockMessagingImageMessageForGroup: MessagingImageMessage = + mock<MessagingImageMessage>().apply { + whenever(drawable).thenReturn(mockDrawableGroupMessage) + } + val mockMessageContainer: MessagingLinearLayout = + mock<MessagingLinearLayout>().apply { + whenever(childCount).thenReturn(1) + whenever(getChildAt(any())).thenReturn(mockMessagingImageMessageForGroup) + } + val mockGroup: MessagingGroup = + mock<MessagingGroup>().apply { + whenever(messageContainer).thenReturn(mockMessageContainer) + } + val mockView: View = + mock<ConversationLayout>().apply { + whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup))) + whenever(imageMessageContainer).thenReturn(mockImageMessageContainer) + whenever(messagingLinearLayout).thenReturn(mockMessageContainer) + + // These must be mocked as they're required to be nonnull. + whenever(requireViewById<View>(R.id.conversation_icon_container)).thenReturn(mock()) + whenever(requireViewById<CachingIconView>(R.id.conversation_icon)) + .thenReturn(mock()) + whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.conversation_icon_badge_ring)) + .thenReturn(mock()) + whenever(requireViewById<View>(R.id.app_name_text)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.conversation_text)).thenReturn(mock()) + } + return mockView + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt new file mode 100644 index 000000000000..c0444b563a2c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt @@ -0,0 +1,103 @@ +/* + * 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.notification.row.wrapper + +import android.graphics.drawable.AnimatedImageDrawable +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.internal.widget.MessagingGroup +import com.android.internal.widget.MessagingImageMessage +import com.android.internal.widget.MessagingLayout +import com.android.internal.widget.MessagingLinearLayout +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() { + + private lateinit var mRow: ExpandableNotificationRow + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + mRow = helper.createRow() + } + + @Test + fun setAnimationsRunning_Run() { + // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type, + // with a mock imageMessage.drawable embedded in its MessagingImageMessage. + val mockDrawable = mock<AnimatedImageDrawable>() + val mockLayoutView: View = fakeMessagingLayout(mockDrawable) + + val wrapper: NotificationViewWrapper = + NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow) + wrapper.setAnimationsRunning(true) + + // Verifies that each AnimatedImageDrawable is started animating. + verify(mockDrawable).start() + } + + @Test + fun setAnimationsRunning_Stop() { + // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type, + // with a mock imageMessage.drawable embedded in its MessagingImageMessage. + val mockDrawable = mock<AnimatedImageDrawable>() + val mockLayoutView: View = fakeMessagingLayout(mockDrawable) + + val wrapper: NotificationViewWrapper = + NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow) + wrapper.setAnimationsRunning(false) + + // Verifies that each AnimatedImageDrawable is started animating. + verify(mockDrawable).stop() + } + + private fun fakeMessagingLayout(mockDrawable: AnimatedImageDrawable): View { + val mockMessagingImageMessage: MessagingImageMessage = + mock<MessagingImageMessage>().apply { whenever(drawable).thenReturn(mockDrawable) } + val mockMessageContainer: MessagingLinearLayout = + mock<MessagingLinearLayout>().apply { + whenever(childCount).thenReturn(1) + whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage) + } + val mockGroup: MessagingGroup = + mock<MessagingGroup>().apply { + whenever(messageContainer).thenReturn(mockMessageContainer) + } + val mockView: View = + mock<MessagingLayout>().apply { + whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup))) + } + return mockView + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index ca99e24fc105..e41929f7d578 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import org.junit.Assert; import org.junit.Before; @@ -216,4 +217,29 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertEquals(1f, mChildrenContainer.getBottomRoundness(), 0.001f); Assert.assertEquals(1f, notificationRow.getBottomRoundness(), 0.001f); } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_header() { + mChildrenContainer.useRoundnessSourceTypes(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { + mChildrenContainer.useRoundnessSourceTypes(true); + mChildrenContainer.setIsLowPriority(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 645052feee94..3d3b91d807c1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -21,6 +21,7 @@ import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; @@ -65,6 +66,7 @@ import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl; import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; +import com.android.systemui.statusbar.notification.collection.render.NotifStats; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -139,6 +141,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; + private final SeenNotificationsProviderImpl mSeenNotificationsProvider = + new SeenNotificationsProviderImpl(); + private NotificationStackScrollLayoutController mController; @Before @@ -180,7 +185,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mUiEventLogger, mRemoteInputManager, mVisibilityLocationProviderDelegator, - new SeenNotificationsProviderImpl(), + mSeenNotificationsProvider, mShadeController, mJankMonitor, mStackLogger, @@ -233,16 +238,14 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ true, - /* notifVisibleInShade= */ true, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ true); setupShowEmptyShadeViewState(false); reset(mNotificationStackScrollLayout); mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ false, - /* notifVisibleInShade= */ true, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ true); } @Test @@ -255,16 +258,14 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ true, - /* notifVisibleInShade= */ false, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ false); setupShowEmptyShadeViewState(false); reset(mNotificationStackScrollLayout); mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ false, - /* notifVisibleInShade= */ false, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ false); } @Test @@ -283,16 +284,14 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ true, - /* notifVisibleInShade= */ false, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ false); mController.setQsFullScreen(true); reset(mNotificationStackScrollLayout); mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ true, - /* notifVisibleInShade= */ false, - /* areSeenNotifsFiltered= */false); + /* notifVisibleInShade= */ false); } @Test @@ -400,6 +399,17 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true); } + @Test + public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() { + when(mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()).thenReturn(true); + mSeenNotificationsProvider.setHasFilteredOutSeenNotifications(true); + mController.attach(mNotificationStackScrollLayout); + mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); + verify(mNotificationStackScrollLayout).setHasFilteredOutSeenNotifications(true); + verify(mNotificationStackScrollLayout).updateFooter(); + verify(mNotificationStackScrollLayout).updateEmptyShadeView(anyBoolean(), anyBoolean()); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 7622549d29de..dd7143ae7e16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -30,6 +30,7 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertFalse; +import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; @@ -53,6 +54,7 @@ import android.util.MathUtils; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; @@ -328,7 +330,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { public void updateEmptyView_dndSuppressing() { when(mEmptyShadeView.willBeGone()).thenReturn(true); - mStackScroller.updateEmptyShadeView(true, true, false); + mStackScroller.updateEmptyShadeView(true, true); verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text); } @@ -338,7 +340,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mEmptyShadeView.willBeGone()).thenReturn(true); - mStackScroller.updateEmptyShadeView(true, false, false); + mStackScroller.updateEmptyShadeView(true, false); verify(mEmptyShadeView).setText(R.string.empty_shade_text); } @@ -347,10 +349,10 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { public void updateEmptyView_noNotificationsToDndSuppressing() { mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mEmptyShadeView.willBeGone()).thenReturn(true); - mStackScroller.updateEmptyShadeView(true, false, false); + mStackScroller.updateEmptyShadeView(true, false); verify(mEmptyShadeView).setText(R.string.empty_shade_text); - mStackScroller.updateEmptyShadeView(true, true, false); + mStackScroller.updateEmptyShadeView(true, true); verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text); } @@ -818,6 +820,29 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { assertEquals(0f, mAmbientState.getStackY()); } + @Test + public void hasFilteredOutSeenNotifs_updateFooter() { + mStackScroller.setCurrentUserSetup(true); + + // add footer + mStackScroller.inflateFooterView(); + TextView footerLabel = + mStackScroller.mFooterView.requireViewById(R.id.unlock_prompt_footer); + + mStackScroller.setHasFilteredOutSeenNotifications(true); + mStackScroller.updateFooter(); + + assertThat(footerLabel.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void hasFilteredOutSeenNotifs_updateEmptyShadeView() { + mStackScroller.setHasFilteredOutSeenNotifications(true); + mStackScroller.updateEmptyShadeView(true, false); + + verify(mEmptyShadeView).setFooterText(not(0)); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java index 4ccbc6d45e63..091bb5455d93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; @@ -74,6 +75,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.mockito.stubbing.Answer; import java.util.Collections; import java.util.List; @@ -115,8 +117,10 @@ public class AutoTileManagerTest extends SysuiTestCase { @Spy private PackageManager mPackageManager; private final boolean mIsReduceBrightColorsAvailable = true; - private AutoTileManager mAutoTileManager; + private AutoTileManager mAutoTileManager; // under test + private SecureSettings mSecureSettings; + private ManagedProfileController.Callback mManagedProfileCallback; @Before public void setUp() throws Exception { @@ -303,7 +307,7 @@ public class AutoTileManagerTest extends SysuiTestCase { InOrder inOrderManagedProfile = inOrder(mManagedProfileController); inOrderManagedProfile.verify(mManagedProfileController).removeCallback(any()); - inOrderManagedProfile.verify(mManagedProfileController, never()).addCallback(any()); + inOrderManagedProfile.verify(mManagedProfileController).addCallback(any()); if (ColorDisplayManager.isNightDisplayAvailable(mContext)) { InOrder inOrderNightDisplay = inOrder(mNightDisplayListener); @@ -504,6 +508,40 @@ public class AutoTileManagerTest extends SysuiTestCase { } @Test + public void managedProfileAdded_tileAdded() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(false); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(true); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).addTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileAdded(eq("work")); + } + + @Test + public void managedProfileRemoved_tileRemoved() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(true); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(false); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).removeTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileRemoved(eq("work")); + } + + @Test public void testEmptyArray_doesNotCrash() { mContext.getOrCreateTestableResources().addOverride( R.array.config_quickSettingsAutoAdd, new String[0]); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 74f8c61ad186..daf7dd06b0d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -57,6 +59,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -122,6 +125,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private VibratorHelper mVibratorHelper; @Mock private BiometricUnlockLogger mLogger; + private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; @Before @@ -144,7 +148,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mMetricsLogger, mDumpManager, mPowerManager, mLogger, mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController, - mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper); + mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, + mSystemClock + ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener); when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker); @@ -207,7 +213,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mKeyguardViewMediator).onWakeAndUnlocking(); assertThat(mBiometricUnlockController.getMode()) - .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); + .isEqualTo(MODE_WAKE_AND_UNLOCK); } @Test @@ -457,4 +463,83 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { // THEN wakeup the device verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); } + + @Test + public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time just occurred + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN DO NOT vibrate the device + verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + // GIVEN side fingerprint enrolled, wakeup just happened + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // GIVEN last wake reason was from a gesture + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_GESTURE); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintFail_alwaysPlaysHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was recent power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint fails + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + + // THEN always vibrate the device + verify(mVibratorHelper).vibrateAuthError(anyString()); + } + + private void givenFingerprintModeUnlockCollapsing() { + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + } } 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 09254ad0faf2..4c1b219af843 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 @@ -110,6 +110,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; @@ -177,8 +178,6 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; -import dagger.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -191,6 +190,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; +import dagger.Lazy; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -297,6 +298,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private WiredChargingRippleController mWiredChargingRippleController; @Mock private Lazy<CameraLauncher> mCameraLauncherLazy; @Mock private CameraLauncher mCameraLauncher; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; /** * The process of registering/unregistering a predictive back callback requires a * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test. @@ -378,7 +380,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); mWakefulnessLifecycle = - new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); + new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock, + mDumpManager); mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); mWakefulnessLifecycle.dispatchFinishedWakingUp(); @@ -504,7 +507,9 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mWiredChargingRippleController, mDreamManager, mCameraLauncherLazy, - () -> mLightRevealScrimViewModel) { + () -> mLightRevealScrimViewModel, + mAlternateBouncerInteractor + ) { @Override protected ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -539,6 +544,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.startKeyguard(); mInitController.executePostInitTasks(); notificationLogger.setUpWithContainer(mNotificationListContainer); + mCentralSurfaces.registerCallbacks(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 077b41a0aa90..c8438501b3e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -23,6 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; @@ -39,10 +43,9 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; @@ -52,6 +55,8 @@ import org.junit.Assert; 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; @@ -69,7 +74,6 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private TunerService mTunerService; @Mock private BatteryController mBatteryController; - @Mock private FeatureFlags mFeatureFlags; @Mock private DumpManager mDumpManager; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private FoldAodAnimationController mFoldAodAnimationController; @@ -78,6 +82,7 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; + @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback; /** * The current value of PowerManager's dozeAfterScreenOff property. @@ -113,7 +118,6 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryController, mTunerService, mDumpManager, - mFeatureFlags, mScreenOffAnimationController, Optional.of(mSysUIUnfoldComponent), mUnlockedScreenOffAnimationController, @@ -122,7 +126,8 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController ); - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true); + verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); + setAodEnabledForTest(true); setShouldControlUnlockedScreenOffForTest(true); setDisplayNeedsBlankingForTest(false); @@ -173,6 +178,29 @@ public class DozeParametersTest extends SysuiTestCase { assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } + @Test + public void testGetAlwaysOn_whenBatterySaverCallback() { + DozeParameters.Callback callback = mock(DozeParameters.Callback.class); + mDozeParameters.addCallback(callback); + + when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mBatteryController.isAodPowerSave()).thenReturn(true); + + // Both lines should trigger an event + mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback, times(2)).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + + reset(callback); + when(mBatteryController.isAodPowerSave()).thenReturn(false); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isTrue(); + } + /** * PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling * it with false means we are. Confusing, but sure - make sure that we call PowerManager with @@ -196,17 +224,6 @@ public class DozeParametersTest extends SysuiTestCase { } @Test - public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() { - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false); - - assertFalse(mDozeParameters.shouldControlUnlockedScreenOff()); - - // Trigger the setter for the current value. - mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff()); - assertFalse(mDozeParameters.shouldControlScreenOff()); - } - - @Test public void propagatesAnimateScreenOff_noAlwaysOn() { setAodEnabledForTest(false); setDisplayNeedsBlankingForTest(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java index df7ee432e79e..2f495355df40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN; -import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; import static com.google.common.truth.Truth.assertThat; @@ -58,8 +58,8 @@ import com.android.systemui.DejankUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.keyguard.DismissCallbackRegistry; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; -import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Assert; @@ -87,7 +87,7 @@ public class KeyguardBouncerTest extends SysuiTestCase { @Mock private KeyguardHostViewController mKeyguardHostViewController; @Mock - private KeyguardBouncer.PrimaryBouncerExpansionCallback mExpansionCallback; + private PrimaryBouncerExpansionCallback mExpansionCallback; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt new file mode 100644 index 000000000000..3e90ed9811d0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.content.pm.PackageManager +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +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.policy.DevicePostureController +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_FLIPPED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.tuner.TunerService +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class KeyguardBypassControllerTest : SysuiTestCase() { + + private lateinit var keyguardBypassController: KeyguardBypassController + private lateinit var postureControllerCallback: DevicePostureController.Callback + @Mock private lateinit var tunerService: TunerService + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock private lateinit var devicePostureController: DevicePostureController + @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var packageManager: PackageManager + @Captor + private val postureCallbackCaptor = + ArgumentCaptor.forClass(DevicePostureController.Callback::class.java) + @JvmField @Rule val mockito = MockitoJUnit.rule() + + @Before + fun setUp() { + context.setMockPackageManager(packageManager) + whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true) + whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true) + } + + @After + fun tearDown() { + reset(devicePostureController) + reset(keyguardStateController) + } + + private fun defaultConfigPostureClosed() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_CLOSED + ) + initKeyguardBypassController() + verify(devicePostureController).addCallback(postureCallbackCaptor.capture()) + postureControllerCallback = postureCallbackCaptor.value + } + + private fun defaultConfigPostureOpened() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_OPENED + ) + initKeyguardBypassController() + verify(devicePostureController).addCallback(postureCallbackCaptor.capture()) + postureControllerCallback = postureCallbackCaptor.value + } + + private fun defaultConfigPostureFlipped() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_FLIPPED + ) + initKeyguardBypassController() + verify(devicePostureController).addCallback(postureCallbackCaptor.capture()) + postureControllerCallback = postureCallbackCaptor.value + } + + private fun defaultConfigPostureUnknown() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN + ) + initKeyguardBypassController() + verify(devicePostureController, never()).addCallback(postureCallbackCaptor.capture()) + } + + private fun initKeyguardBypassController() { + keyguardBypassController = + KeyguardBypassController( + context, + tunerService, + statusBarStateController, + lockscreenUserManager, + keyguardStateController, + shadeExpansionStateManager, + devicePostureController, + dumpManager + ) + } + + @Test + fun configDevicePostureClosed_matchState_isPostureAllowedForFaceAuth_returnTrue() { + defaultConfigPostureClosed() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue() + } + + @Test + fun configDevicePostureOpen_matchState_isPostureAllowedForFaceAuth_returnTrue() { + defaultConfigPostureOpened() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue() + } + + @Test + fun configDevicePostureFlipped_matchState_isPostureAllowedForFaceAuth_returnTrue() { + defaultConfigPostureFlipped() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue() + } + + @Test + fun configDevicePostureClosed_changeOpened_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureClosed() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun configDevicePostureClosed_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureClosed() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun configDevicePostureOpened_changeClosed_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureOpened() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun configDevicePostureOpened_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureOpened() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun configDevicePostureFlipped_changeClosed_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureFlipped() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun configDevicePostureFlipped_changeOpened_isPostureAllowedForFaceAuth_returnFalse() { + defaultConfigPostureFlipped() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse() + } + + @Test + fun defaultConfigPostureClosed_canOverrideByPassAlways_shouldReturnFalse() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + 1 /* FACE_UNLOCK_BYPASS_ALWAYS */ + ) + + defaultConfigPostureClosed() + + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + assertThat(keyguardBypassController.bypassEnabled).isFalse() + } + + @Test + fun defaultConfigPostureUnknown_canNotOverrideByPassAlways_shouldReturnTrue() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + 1 /* FACE_UNLOCK_BYPASS_ALWAYS */ + ) + + defaultConfigPostureUnknown() + + assertThat(keyguardBypassController.bypassEnabled).isTrue() + } + + @Test + fun defaultConfigPostureUnknown_canNotOverrideByPassNever_shouldReturnFalse() { + context.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + 2 /* FACE_UNLOCK_BYPASS_NEVER */ + ) + + defaultConfigPostureUnknown() + + assertThat(keyguardBypassController.bypassEnabled).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index e4759057a59c..c7a0582f0007 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -58,6 +58,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -1562,7 +1563,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Test public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); - mScrimController.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN); + mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); mScrimController.transitionTo(ScrimState.DREAMING); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt new file mode 100644 index 000000000000..3bc288a2f823 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.os.UserHandle +import androidx.test.filters.SmallTest +import com.android.internal.statusbar.StatusBarIcon +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY +import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.verify + +@SmallTest +class StatusBarIconControllerImplTest : SysuiTestCase() { + + private lateinit var underTest: StatusBarIconControllerImpl + + private lateinit var iconList: StatusBarIconList + private val iconGroup: StatusBarIconController.IconManager = mock() + + @Before + fun setUp() { + iconList = StatusBarIconList(arrayOf()) + underTest = + StatusBarIconControllerImpl( + context, + mock(), + mock(), + mock(), + mock(), + mock(), + iconList, + mock(), + ) + underTest.addIconGroup(iconGroup) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_bothDisplayed() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + val externalIcon = + StatusBarIcon( + "external.package", + UserHandle.ALL, + /* iconId= */ 2, + /* iconLevel= */ 0, + /* number= */ 0, + "contentDescription", + ) + underTest.setIcon(slotName, externalIcon) + + assertThat(iconList.slots).hasSize(2) + // Whichever was added last comes first + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isTrue() + assertThat(iconList.slots[1].hasIconsInSlot()).isTrue() + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_externalRemoved_viaRemoveIcon_internalStays() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + underTest.setIcon(slotName, createExternalIcon()) + + // WHEN the external icon is removed via #removeIcon + underTest.removeIcon(slotName) + + // THEN the external icon is removed but the internal icon remains + // Note: [StatusBarIconList] never removes slots from its list, it just sets the holder for + // the slot to null when an icon is removed. + assertThat(iconList.slots).hasSize(2) + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal + assertThat(iconList.slots[1].hasIconsInSlot()).isTrue() + + verify(iconGroup).onRemoveIcon(0) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_externalRemoved_viaRemoveAll_internalStays() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + underTest.setIcon(slotName, createExternalIcon()) + + // WHEN the external icon is removed via #removeAllIconsForExternalSlot + underTest.removeAllIconsForExternalSlot(slotName) + + // THEN the external icon is removed but the internal icon remains + assertThat(iconList.slots).hasSize(2) + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal + assertThat(iconList.slots[1].hasIconsInSlot()).isTrue() + + verify(iconGroup).onRemoveIcon(0) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_externalRemoved_viaSetNull_internalStays() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + underTest.setIcon(slotName, createExternalIcon()) + + // WHEN the external icon is removed via a #setIcon(null) + underTest.setIcon(slotName, /* icon= */ null) + + // THEN the external icon is removed but the internal icon remains + assertThat(iconList.slots).hasSize(2) + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal + assertThat(iconList.slots[1].hasIconsInSlot()).isTrue() + + verify(iconGroup).onRemoveIcon(0) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_internalRemoved_viaRemove_externalStays() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + underTest.setIcon(slotName, createExternalIcon()) + + // WHEN the internal icon is removed via #removeIcon + underTest.removeIcon(slotName, /* tag= */ 0) + + // THEN the external icon is removed but the internal icon remains + assertThat(iconList.slots).hasSize(2) + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isTrue() + assertThat(iconList.slots[1].hasIconsInSlot()).isFalse() // Indicates removal + + verify(iconGroup).onRemoveIcon(1) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_internalRemoved_viaRemoveAll_externalStays() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + underTest.setIcon(slotName, createExternalIcon()) + + // WHEN the internal icon is removed via #removeAllIconsForSlot + underTest.removeAllIconsForSlot(slotName) + + // THEN the external icon is removed but the internal icon remains + assertThat(iconList.slots).hasSize(2) + assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(iconList.slots[1].name).isEqualTo(slotName) + assertThat(iconList.slots[0].hasIconsInSlot()).isTrue() + assertThat(iconList.slots[1].hasIconsInSlot()).isFalse() // Indicates removal + + verify(iconGroup).onRemoveIcon(1) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_internalUpdatedIndependently() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + val startingExternalIcon = + StatusBarIcon( + "external.package", + UserHandle.ALL, + /* iconId= */ 20, + /* iconLevel= */ 0, + /* number= */ 0, + "externalDescription", + ) + underTest.setIcon(slotName, startingExternalIcon) + + // WHEN the internal icon is updated + underTest.setIcon(slotName, /* resourceId= */ 11, "newContentDescription") + + // THEN only the internal slot gets the updates + val internalSlot = iconList.slots[1] + val internalHolder = internalSlot.getHolderForTag(TAG_PRIMARY)!! + assertThat(internalSlot.name).isEqualTo(slotName) + assertThat(internalHolder.icon!!.contentDescription).isEqualTo("newContentDescription") + assertThat(internalHolder.icon!!.icon.resId).isEqualTo(11) + + // And the external slot has its own values + val externalSlot = iconList.slots[0] + val externalHolder = externalSlot.getHolderForTag(TAG_PRIMARY)!! + assertThat(externalSlot.name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(externalHolder.icon!!.contentDescription).isEqualTo("externalDescription") + assertThat(externalHolder.icon!!.icon.resId).isEqualTo(20) + } + + /** Regression test for b/255428281. */ + @Test + fun internalAndExternalIconWithSameName_externalUpdatedIndependently() { + val slotName = "mute" + + // Internal + underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription") + + // External + val startingExternalIcon = + StatusBarIcon( + "external.package", + UserHandle.ALL, + /* iconId= */ 20, + /* iconLevel= */ 0, + /* number= */ 0, + "externalDescription", + ) + underTest.setIcon(slotName, startingExternalIcon) + + // WHEN the external icon is updated + val newExternalIcon = + StatusBarIcon( + "external.package", + UserHandle.ALL, + /* iconId= */ 21, + /* iconLevel= */ 0, + /* number= */ 0, + "newExternalDescription", + ) + underTest.setIcon(slotName, newExternalIcon) + + // THEN only the external slot gets the updates + val externalSlot = iconList.slots[0] + val externalHolder = externalSlot.getHolderForTag(TAG_PRIMARY)!! + assertThat(externalSlot.name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX) + assertThat(externalHolder.icon!!.contentDescription).isEqualTo("newExternalDescription") + assertThat(externalHolder.icon!!.icon.resId).isEqualTo(21) + + // And the internal slot has its own values + val internalSlot = iconList.slots[1] + val internalHolder = internalSlot.getHolderForTag(TAG_PRIMARY)!! + assertThat(internalSlot.name).isEqualTo(slotName) + assertThat(internalHolder.icon!!.contentDescription).isEqualTo("contentDescription") + assertThat(internalHolder.icon!!.icon.resId).isEqualTo(10) + } + + @Test + fun externalSlot_alreadyEndsWithSuffix_suffixNotAddedTwice() { + underTest.setIcon("myslot$EXTERNAL_SLOT_SUFFIX", createExternalIcon()) + + assertThat(iconList.slots).hasSize(1) + assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX") + } + + private fun createExternalIcon(): StatusBarIcon { + return StatusBarIcon( + "external.package", + UserHandle.ALL, + /* iconId= */ 2, + /* iconLevel= */ 0, + /* number= */ 0, + "contentDescription", + ) + } +} 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 14a319bc87e9..1ba0a36fb05d 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 @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.flags.Flags.MODERN_BOUNCER; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -37,6 +39,8 @@ import android.testing.TestableLooper; import android.view.View; import android.view.ViewGroup; import android.view.ViewRootImpl; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; @@ -54,9 +58,12 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dock.DockManager; 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.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; @@ -105,7 +112,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory; @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; - @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; @Mock private KeyguardMessageArea mKeyguardMessageArea; @Mock private ShadeController mShadeController; @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent; @@ -115,18 +121,23 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; + @Mock private OnBackAnimationCallback mBouncerViewDelegateBackCallback; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback; + private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback + mBouncerExpansionCallback; private FakeKeyguardStateController mKeyguardStateController = spy(new FakeKeyguardStateController()); - @Mock private ViewRootImpl mViewRootImpl; - @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; + @Mock + private ViewRootImpl mViewRootImpl; + @Mock + private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; @Captor - private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; + private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor; @Before @@ -137,6 +148,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class))) .thenReturn(mKeyguardMessageAreaController); when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate); + when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback); + when(mFeatureFlags + .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM)) + .thenReturn(true); when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(true); @@ -163,7 +178,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -179,8 +195,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mNotificationContainer, mBypassController); mStatusBarKeyguardViewManager.show(null); - ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor = - ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class); + ArgumentCaptor<PrimaryBouncerExpansionCallback> callbackArgumentCaptor = + ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class); verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback( callbackArgumentCaptor.capture()); mBouncerExpansionCallback = callbackArgumentCaptor.getValue(); @@ -189,7 +205,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void dismissWithAction_AfterKeyguardGoneSetToFalse() { OnDismissAction action = () -> false; - Runnable cancelAction = () -> {}; + Runnable cancelAction = () -> { + }; mStatusBarKeyguardViewManager.dismissWithAction( action, cancelAction, false /* afterKeyguardGone */); verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction)); @@ -253,7 +270,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN)); + verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(EXPANSION_HIDDEN)); } @Test @@ -281,7 +298,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); @@ -298,7 +315,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); @@ -309,7 +326,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mKeyguardStateController.isOccluded()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); @@ -326,7 +343,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); @@ -337,7 +354,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat()); @@ -434,37 +451,35 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); assertTrue( - "Is showing not accurate when alternative auth showing", + "Is showing not accurate when alternative bouncer is visible", mStatusBarKeyguardViewManager.isBouncerShowing()); } @Test public void testWillBeShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); assertTrue( - "Is or will be showing not accurate when alternative auth showing", + "Is or will be showing not accurate when alternate bouncer is visible", mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); } @Test - public void testHideAlternateBouncer_onShowBouncer() { - // GIVEN alt auth is showing - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + public void testHideAlternateBouncer_onShowPrimaryBouncer() { + reset(mAlternateBouncerInteractor); + + // GIVEN alt bouncer is showing when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - reset(mAlternateBouncer); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN showBouncer is called mStatusBarKeyguardViewManager.showPrimaryBouncer(true); // THEN alt bouncer should be hidden - verify(mAlternateBouncer).hideAlternateBouncer(); + verify(mAlternateBouncerInteractor).hide(); } @Test @@ -479,11 +494,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testShowAltAuth_unlockingWithBiometricNotAllowed() { - // GIVEN alt auth exists, unlocking with biometric isn't allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + // GIVEN cannot use alternate bouncer when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false); + when(mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()).thenReturn(false); // WHEN showGenericBouncer is called final boolean scrimmed = true; @@ -491,21 +504,19 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // THEN regular bouncer is shown verify(mPrimaryBouncerInteractor).show(eq(scrimmed)); - verify(mAlternateBouncer, never()).showAlternateBouncer(); } @Test public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { - // GIVEN alt auth exists, unlocking with biometric is allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + // GIVEN will show alternate bouncer when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mAlternateBouncerInteractor.show()).thenReturn(true); // WHEN showGenericBouncer is called mStatusBarKeyguardViewManager.showBouncer(true); // THEN alt auth bouncer is shown - verify(mAlternateBouncer).showAlternateBouncer(); + verify(mAlternateBouncerInteractor).show(); verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); } @@ -543,12 +554,12 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerExpansionCallback.onVisibilityChanged(true); verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), - mOnBackInvokedCallback.capture()); + mBackCallbackCaptor.capture()); /* verify that the same callback is unregistered when the bouncer becomes invisible */ mBouncerExpansionCallback.onVisibilityChanged(false); verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback( - eq(mOnBackInvokedCallback.getValue())); + eq(mBackCallbackCaptor.getValue())); } @Test @@ -557,18 +568,63 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { /* capture the predictive back callback during registration */ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), - mOnBackInvokedCallback.capture()); + mBackCallbackCaptor.capture()); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true); /* invoke the back callback directly */ - mOnBackInvokedCallback.getValue().onBackInvoked(); + mBackCallbackCaptor.getValue().onBackInvoked(); /* verify that the bouncer will be hidden as a result of the invocation */ verify(mCentralSurfaces).setBouncerShowing(eq(false)); } @Test + public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() { + when(mKeyguardSecurityModel.getSecurityMode(anyInt())) + .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin); + mBouncerExpansionCallback.onVisibilityChanged(true); + /* capture the predictive back callback during registration */ + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mBackCallbackCaptor.capture()); + assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback); + + OnBackAnimationCallback backCallback = + (OnBackAnimationCallback) mBackCallbackCaptor.getValue(); + + BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT); + backCallback.onBackStarted(event); + verify(mBouncerViewDelegateBackCallback, never()).onBackStarted(any()); + } + + @Test + public void testPredictiveBackCallback_forwardsBackDispatches() { + mBouncerExpansionCallback.onVisibilityChanged(true); + /* capture the predictive back callback during registration */ + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mBackCallbackCaptor.capture()); + assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback); + + OnBackAnimationCallback backCallback = + (OnBackAnimationCallback) mBackCallbackCaptor.getValue(); + + BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT); + backCallback.onBackStarted(event); + verify(mBouncerViewDelegateBackCallback).onBackStarted(eq(event)); + + backCallback.onBackProgressed(event); + verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event)); + + backCallback.onBackInvoked(); + verify(mBouncerViewDelegateBackCallback).onBackInvoked(); + + backCallback.onBackCancelled(); + verify(mBouncerViewDelegateBackCallback).onBackCancelled(); + } + + @Test public void testReportBouncerOnDreamWhenVisible() { mBouncerExpansionCallback.onVisibilityChanged(true); verify(mCentralSurfaces).setBouncerShowingOverDream(false); @@ -613,7 +669,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java index 96fba39d6b59..55ab6812da91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.flags.Flags.MODERN_BOUNCER; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -56,7 +58,9 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.data.BouncerView; import com.android.systemui.keyguard.data.BouncerViewDelegate; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; @@ -109,7 +113,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; @Mock private KeyguardBouncer mPrimaryBouncer; - @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; @Mock private KeyguardMessageArea mKeyguardMessageArea; @Mock private ShadeController mShadeController; @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent; @@ -119,11 +122,13 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback; + private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback + mBouncerExpansionCallback; private FakeKeyguardStateController mKeyguardStateController = spy(new FakeKeyguardStateController()); @@ -138,7 +143,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mKeyguardBouncerFactory.create( any(ViewGroup.class), - any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class))) + any(PrimaryBouncerExpansionCallback.class))) .thenReturn(mPrimaryBouncer); when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer); when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea); @@ -169,7 +174,8 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -185,8 +191,8 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { mNotificationContainer, mBypassController); mStatusBarKeyguardViewManager.show(null); - ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor = - ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class); + ArgumentCaptor<PrimaryBouncerExpansionCallback> callbackArgumentCaptor = + ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class); verify(mKeyguardBouncerFactory).create(any(ViewGroup.class), callbackArgumentCaptor.capture()); mBouncerExpansionCallback = callbackArgumentCaptor.getValue(); @@ -259,7 +265,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { when(mPrimaryBouncer.inTransit()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN)); + verify(mPrimaryBouncer).setExpansion(eq(EXPANSION_HIDDEN)); } @Test @@ -287,7 +293,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); @@ -304,7 +310,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); @@ -315,7 +321,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { when(mKeyguardStateController.isOccluded()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); @@ -332,7 +338,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); @@ -343,7 +349,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); mStatusBarKeyguardViewManager.onPanelExpansionChanged( expansionEvent( - /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* fraction= */ EXPANSION_VISIBLE, /* expanded= */ true, /* tracking= */ false)); verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); @@ -439,41 +445,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { } @Test - public void testShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - assertTrue( - "Is showing not accurate when alternative auth showing", - mStatusBarKeyguardViewManager.isBouncerShowing()); - } - - @Test - public void testWillBeShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - assertTrue( - "Is or will be showing not accurate when alternative auth showing", - mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); - } - - @Test - public void testHideAlternateBouncer_onShowBouncer() { - // GIVEN alt auth is showing - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - reset(mAlternateBouncer); - - // WHEN showBouncer is called - mStatusBarKeyguardViewManager.showPrimaryBouncer(true); - - // THEN alt bouncer should be hidden - verify(mAlternateBouncer).hideAlternateBouncer(); - } - - @Test public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() { when(mPrimaryBouncer.isShowing()).thenReturn(false); when(mPrimaryBouncer.inTransit()).thenReturn(true); @@ -484,38 +455,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { } @Test - public void testShowAltAuth_unlockingWithBiometricNotAllowed() { - // GIVEN alt auth exists, unlocking with biometric isn't allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false); - - // WHEN showGenericBouncer is called - final boolean scrimmed = true; - mStatusBarKeyguardViewManager.showBouncer(scrimmed); - - // THEN regular bouncer is shown - verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed)); - verify(mAlternateBouncer, never()).showAlternateBouncer(); - } - - @Test - public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { - // GIVEN alt auth exists, unlocking with biometric is allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); - - // WHEN showGenericBouncer is called - mStatusBarKeyguardViewManager.showBouncer(true); - - // THEN alt auth bouncer is shown - verify(mAlternateBouncer).showAlternateBouncer(); - verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); - } - - @Test public void testUpdateResources_delegatesToBouncer() { mStatusBarKeyguardViewManager.updateResources(); @@ -628,7 +567,8 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 49d4bdc88c82..0add905e2750 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -26,6 +26,7 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository @@ -56,6 +57,10 @@ class FakeMobileConnectionsRepository( private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId + override val activeSubChangedInGroupEvent: MutableSharedFlow<Unit> = MutableSharedFlow() + + private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + override val defaultDataSubId = _defaultDataSubId private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) override val defaultMobileNetworkConnectivity = _mobileConnectivity @@ -81,6 +86,10 @@ class FakeMobileConnectionsRepository( _subscriptions.value = subs } + fun setDefaultDataSubId(id: Int) { + _defaultDataSubId.value = id + } + fun setMobileConnectivity(model: MobileConnectivityModel) { _mobileConnectivity.value = model } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 5d377a8658a5..0859d140c3b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.valid import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock @@ -71,8 +73,10 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var underTest: MobileRepositorySwitcher private lateinit var realRepo: MobileConnectionsRepositoryImpl private lateinit var demoRepo: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource private lateinit var logFactory: TableLogBufferFactory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @@ -96,10 +100,15 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) - mockDataSource = + mobileDataSource = mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow) } + wifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(MutableStateFlow(null)) + } + wifiRepository = FakeWifiRepository() realRepo = MobileConnectionsRepositoryImpl( @@ -113,12 +122,14 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { context, IMMEDIATE, scope, + wifiRepository, mock(), ) demoRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = scope, context = context, logFactory = logFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 210208532dd4..6989b514a703 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -29,6 +29,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectio import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -63,10 +65,12 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null) private lateinit var connectionsRepo: DemoMobileConnectionsRepository private lateinit var underTest: DemoMobileConnectionRepository private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mockWifiDataSource: DemoModeWifiDataSource @Before fun setUp() { @@ -75,10 +79,15 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + mockWifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } connectionsRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mockDataSource, + wifiDataSource = mockWifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index cdbe75e855bc..f12d113cfaa8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -32,6 +32,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionMod import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -57,21 +59,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null) private lateinit var underTest: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource @Before fun setUp() { // The data source only provides one API, so we can mock it with a flow here for convenience - mockDataSource = + mobileDataSource = mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + wifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } underTest = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, @@ -81,6 +90,14 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `connectivity - defaults to connected and validated`() = + testScope.runTest { + val connectivity = underTest.defaultMobileNetworkConnectivity.value + assertThat(connectivity.isConnected).isTrue() + assertThat(connectivity.isValidated).isTrue() + } + + @Test fun `network event - create new subscription`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -97,6 +114,22 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `wifi carrier merged event - create new subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + + @Test fun `network event - reuses subscription when same Id`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -119,6 +152,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `wifi carrier merged event - reuses subscription when same Id`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + // Second network event comes in with the same subId, does not create a new subscription + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + + @Test fun `multiple subscriptions`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -133,6 +188,35 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `mobile subscription and carrier merged subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(2) + + job.cancel() + } + + @Test + fun `multiple mobile subscriptions and carrier merged subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3) + + assertThat(latest).hasSize(3) + + job.cancel() + } + + @Test fun `mobile disabled event - disables connection - subId specified - single conn`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -194,6 +278,112 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `wifi network updates to disabled - carrier merged connection removed`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `wifi network updates to active - carrier merged connection removed`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 1, + activity = 0, + ssid = null, + validated = true, + ) + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged - only one connection`() = + testScope.runTest { + var latestSubsList: List<SubscriptionModel>? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2) + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + val connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged then back - has old mobile data`() = + testScope.runTest { + var latestSubsList: List<SubscriptionModel>? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + val mobileEvent = validMobileEvent(subId = 3, level = 2) + fakeNetworkEventFlow.value = mobileEvent + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + var connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + // WHEN the carrier merged is removed + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 4, + activity = 0, + ssid = null, + validated = true, + ) + + // THEN the subId=3 connection goes back to the mobile information + connection = connections!!.find { it.subId == 3 }!! + assertConnection(connection, mobileEvent) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun `multiple connections - remove all - does not throw`() = @@ -289,6 +479,51 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `demo connection - two connections - update carrier merged - no affect on first`() = + testScope.runTest { + var currentEvent1 = validMobileEvent(subId = 1) + var connection1: DemoMobileConnectionRepository? = null + var currentEvent2 = validCarrierMergedEvent(subId = 2) + var connection2: DemoMobileConnectionRepository? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = currentEvent1 + fakeWifiEventFlow.value = currentEvent2 + assertThat(connections).hasSize(2) + connections!!.forEach { + when (it.subId) { + 1 -> connection1 = it + 2 -> connection2 = it + else -> Assert.fail("Unexpected subscription") + } + } + + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // WHEN the event changes for connection 2, it updates, and connection 1 stays the same + currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4) + fakeWifiEventFlow.value = currentEvent2 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // and vice versa + currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) + fakeNetworkEventFlow.value = currentEvent1 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + job.cancel() + } + private fun assertConnection( conn: DemoMobileConnectionRepository, model: FakeNetworkEventModel @@ -315,6 +550,21 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { else -> {} } } + + private fun assertCarrierMergedConnection( + conn: DemoMobileConnectionRepository, + model: FakeWifiEventModel.CarrierMerged, + ) { + val connectionInfo: MobileConnectionModel = conn.connectionInfo.value + assertThat(conn.subId).isEqualTo(model.subscriptionId) + assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) + assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) + assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false) + assertThat(connectionInfo.isRoaming).isEqualTo(false) + assertThat(connectionInfo.isEmergencyOnly).isFalse() + assertThat(connectionInfo.isGsm).isFalse() + assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected) + } } /** Convenience to create a valid fake network event with minimal params */ @@ -339,3 +589,14 @@ fun validMobileEvent( roaming = roaming, name = "demo name", ) + +fun validCarrierMergedEvent( + subId: Int = 1, + level: Int = 1, + numberOfLevels: Int = 4, +): FakeWifiEventModel.CarrierMerged = + FakeWifiEventModel.CarrierMerged( + subscriptionId = subId, + level = level, + numberOfLevels = numberOfLevels, + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt new file mode 100644 index 000000000000..ea90150b432a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -0,0 +1,251 @@ +/* + * 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.mobile.data.repository.prod + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: CarrierMergedConnectionRepository + + private lateinit var wifiRepository: FakeWifiRepository + @Mock private lateinit var logger: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + wifiRepository = FakeWifiRepository() + + underTest = + CarrierMergedConnectionRepository( + SUB_ID, + logger, + NetworkNameModel.Default("name"), + testScope.backgroundScope, + wifiRepository, + ) + } + + @Test + fun connectionInfo_inactiveWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_activeWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1)) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + wifiRepository.setIsWifiDefault(true) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + + val expected = + MobileConnectionModel( + primaryLevel = 3, + cdmaLevel = 3, + dataConnectionState = DataConnectionState.Connected, + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + isRoaming = false, + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID + 10, + level = 3, + ) + ) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest!!.primaryLevel).isNotEqualTo(3) + assertThat(latest!!.resolvedNetworkType) + .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButNotEnabled_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiEnabled(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiDefault(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun numberOfLevels_comesFromCarrierMerged() = + testScope.runTest { + var latest: Int? = null + val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 1, + numberOfLevels = 6, + ) + ) + + assertThat(latest).isEqualTo(6) + + job.cancel() + } + + @Test + fun dataEnabled_matchesWifiEnabled() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + assertThat(latest).isTrue() + + wifiRepository.setIsWifiEnabled(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun cdmaRoaming_alwaysFalse() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + private companion object { + const val SUB_ID = 123 + const val NET_ID = 456 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt new file mode 100644 index 000000000000..c02a4dfd074c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** + * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the + * repository interface it's switching on. These tests just need to verify that the entire interface + * properly switches over when the value of `isCarrierMerged` changes. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class FullMobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: FullMobileConnectionRepository + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val mobileMappings = FakeMobileMappingsProxy() + private val tableLogBuffer = mock<TableLogBuffer>() + private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>() + private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>() + + private lateinit var connectionsRepo: FakeMobileConnectionsRepository + private val globalMobileDataSettingChangedEvent: Flow<Unit> + get() = connectionsRepo.globalMobileDataSettingChangedEvent + + private lateinit var mobileRepo: FakeMobileConnectionRepository + private lateinit var carrierMergedRepo: FakeMobileConnectionRepository + + @Before + fun setUp() { + connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer) + + mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + + whenever( + mobileFactory.build( + eq(SUB_ID), + any(), + eq(DEFAULT_NAME), + eq(SEP), + eq(globalMobileDataSettingChangedEvent), + ) + ) + .thenReturn(mobileRepo) + whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME))) + .thenReturn(carrierMergedRepo) + } + + @Test + fun startingIsCarrierMerged_usesCarrierMergedInitially() = + testScope.runTest { + val carrierMergedConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + ) + carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo) + + initializeRepo(startingIsCarrierMerged = true) + + assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo) + verify(mobileFactory, never()) + .build( + SUB_ID, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent + ) + } + + @Test + fun startingNotCarrierMerged_usesTypicalInitially() = + testScope.runTest { + val mobileConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + ) + mobileRepo.setConnectionInfo(mobileConnectionInfo) + + initializeRepo(startingIsCarrierMerged = false) + + assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo) + verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME) + } + + @Test + fun activeRepo_matchesIsCarrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + var latest: MobileConnectionRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + underTest.setIsCarrierMerged(false) + + assertThat(latest).isEqualTo(mobileRepo) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 1, + ) + carrierMergedRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #2", + primaryLevel = 2, + ) + carrierMergedRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #3", + primaryLevel = 3, + ) + carrierMergedRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_mobile() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(false) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator", + primaryLevel = 1, + ) + mobileRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #2", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #3", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_updatesWhenCarrierMergedUpdates() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 4, + ) + carrierMergedRepo.setConnectionInfo(carrierMergedInfo) + + val mobileInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(mobileInfo) + + // Start with the mobile info + assertThat(latest).isEqualTo(mobileInfo) + + // WHEN isCarrierMerged is set to true + underTest.setIsCarrierMerged(true) + + // THEN the carrier merged info is used + assertThat(latest).isEqualTo(carrierMergedInfo) + + val newCarrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "New CM Operator", + primaryLevel = 0, + ) + carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo) + + assertThat(latest).isEqualTo(newCarrierMergedInfo) + + // WHEN isCarrierMerged is set to false + underTest.setIsCarrierMerged(false) + + // THEN the typical info is used + assertThat(latest).isEqualTo(mobileInfo) + + val newMobileInfo = + MobileConnectionModel( + operatorAlphaShort = "New Mobile Operator", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(newMobileInfo) + + assertThat(latest).isEqualTo(newMobileInfo) + + job.cancel() + } + + @Test + fun `factory - reuses log buffers for same connection`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + // Create two connections for the same subId. Similar to if the connection appeared + // and disappeared from the connectionFactory's perspective + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + @Test + fun `factory - reuses log buffers for same sub ID even if carrier merged`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // WHEN a connection with the same sub ID but carrierMerged = true is created + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = true, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // THEN the same table is re-used + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo + // implements logging). + + private fun initializeRepo(startingIsCarrierMerged: Boolean) { + underTest = + FullMobileConnectionRepository( + SUB_ID, + startingIsCarrierMerged, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + testScope.backgroundScope, + mobileFactory, + carrierMergedFactory, + ) + } + + private companion object { + const val SUB_ID = 42 + private val DEFAULT_NAME = NetworkNameModel.Default("default name") + private const val SEP = "-" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 0da15e239932..ae390a0e2959 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -38,8 +38,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -72,6 +75,9 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory + private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory + private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @@ -94,10 +100,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } - whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ -> mock<TableLogBuffer>() } + wifiRepository = FakeWifiRepository() + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -108,7 +116,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logger = logger, mobileMappingsProxy = mobileMappings, scope = scope, + ) + carrierMergedFactory = + CarrierMergedConnectionRepository.Factory( + scope, + wifiRepository, + ) + fullConnectionFactory = + FullMobileConnectionRepository.Factory( + scope = scope, logFactory = logBufferFactory, + mobileRepoFactory = connectionFactory, + carrierMergedRepoFactory = carrierMergedFactory, ) underTest = @@ -123,7 +142,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) } @@ -178,6 +198,40 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionModel>? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_CM)) + + job.cancel() + } + + @Test + fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionModel>? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM)) + + job.cancel() + } + + @Test fun testActiveDataSubscriptionId_initialValueIsInvalidId() = runBlocking(IMMEDIATE) { assertThat(underTest.activeMobileDataSubscriptionId.value) @@ -217,6 +271,96 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testConnectionRepository_carrierMergedSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_CM_ID) + val repo2 = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be not carrier merged + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1)) + + // THEN the repos update + val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be carrier merged + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + + // THEN the repos update + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test fun testConnectionCache_clearsInvalidSubscriptions() = runBlocking(IMMEDIATE) { val job = underTest.subscriptions.launchIn(this) @@ -242,6 +386,34 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged) + + // SUB_2 and SUB_CM disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = @@ -292,14 +464,14 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // Get repos to trigger creation underTest.getRepoForSubId(SUB_1_ID) verify(logBufferFactory) - .create( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), + .getOrCreate( + eq(tableBufferLogName(SUB_1_ID)), anyInt(), ) underTest.getRepoForSubId(SUB_2_ID) verify(logBufferFactory) - .create( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), + .getOrCreate( + eq(tableBufferLogName(SUB_2_ID)), anyInt(), ) @@ -307,6 +479,35 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testDefaultDataSubId_updatesOnBroadcast() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_2_ID) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_1_ID) + + job.cancel() + } + + @Test fun mobileConnectivity_default() { assertThat(underTest.defaultMobileNetworkConnectivity.value) .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) @@ -419,7 +620,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) var latest: MobileMappings.Config? = null @@ -529,5 +731,16 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val NET_ID = 123 private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } + + private const val SUB_CM_ID = 5 + private val SUB_CM = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } + private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) + private val WIFI_NETWORK_CM = + WifiNetworkModel.CarrierMerged( + networkId = 3, + subscriptionId = SUB_CM_ID, + level = 1, + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index a29146b01668..7aeaa48165aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -40,6 +40,8 @@ class FakeMobileIconInteractor( ) ) + override val isConnected = MutableStateFlow(true) + private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G) override val networkTypeIconGroup = _iconGroup diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 1c0064610c52..172755cb8d61 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -23,6 +23,7 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow @@ -59,6 +60,9 @@ class FakeMobileIconsInteractor( override val alwaysShowDataRatIcon = MutableStateFlow(false) override val alwaysUseCdmaLevel = MutableStateFlow(false) + override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID) + + override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping @@ -77,6 +81,8 @@ class FakeMobileIconsInteractor( companion object { val DEFAULT_ICON = TelephonyIcons.G + const val DEFAULT_DATA_SUB_ID = 1 + // Use [MobileMappings] to define some simple definitions const val THREE_G = NETWORK_TYPE_GSM const val LTE = NETWORK_TYPE_LTE diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 61e13b85db6c..c42aba5a7dd9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -60,8 +61,10 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.alwaysShowDataRatIcon, mobileIconsInteractor.alwaysUseCdmaLevel, + mobileIconsInteractor.defaultMobileNetworkConnectivity, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, + mobileIconsInteractor.defaultDataSubId, mobileIconsInteractor.isDefaultConnectionFailed, connectionRepository, ) @@ -271,6 +274,47 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test + fun iconGroup_carrierMerged_usesOverride() = + runBlocking(IMMEDIATE) { + connectionRepository.setConnectionInfo( + MobileConnectionModel( + resolvedNetworkType = CarrierMergedNetworkType, + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride) + + job.cancel() + } + + @Test + fun `icon group - checks default data`() = + runBlocking(IMMEDIATE) { + mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID + connectionRepository.setConnectionInfo( + MobileConnectionModel( + resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.THREE_G) + + // Default data sub id changes to something else + mobileIconsInteractor.defaultDataSubId.value = 123 + yield() + + assertThat(latest).isEqualTo(TelephonyIcons.NOT_DEFAULT_DATA) + + job.cancel() + } + + @Test fun alwaysShowDataRatIcon_matchesParent() = runBlocking(IMMEDIATE) { var latest: Boolean? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index b82a58414db9..1b62d5cc15b5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -31,11 +31,13 @@ import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock 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 kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import org.junit.After import org.junit.Before @@ -43,13 +45,16 @@ import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor private lateinit var connectionsRepository: FakeMobileConnectionsRepository private val userSetupRepository = FakeUserSetupRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val scope = CoroutineScope(IMMEDIATE) + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker @@ -73,7 +78,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { connectionsRepository, carrierConfigTracker, userSetupRepository, - scope + testScope.backgroundScope, ) } @@ -81,7 +86,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_default() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: List<SubscriptionModel>? = null val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) @@ -92,7 +97,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = - runBlocking(IMMEDIATE) { + testScope.runTest { connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) var latest: List<SubscriptionModel>? = null @@ -105,7 +110,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() = - runBlocking(IMMEDIATE) { + testScope.runTest { connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) @@ -122,7 +127,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() = - runBlocking(IMMEDIATE) { + testScope.runTest { connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) @@ -139,7 +144,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() = - runBlocking(IMMEDIATE) { + testScope.runTest { connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) @@ -157,7 +162,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() = - runBlocking(IMMEDIATE) { + testScope.runTest { connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) @@ -175,7 +180,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_turnedOn() = - runBlocking(IMMEDIATE) { + testScope.runTest { CONNECTION_1.setDataEnabled(true) var latest: Boolean? = null val job = @@ -188,7 +193,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_turnedOff() = - runBlocking(IMMEDIATE) { + testScope.runTest { CONNECTION_1.setDataEnabled(true) var latest: Boolean? = null val job = @@ -204,7 +209,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_invalidSubId() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) @@ -220,7 +225,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_connected_validated_notFailed() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true)) @@ -233,7 +238,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_notConnected_notValidated_notFailed() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) @@ -247,7 +252,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_connected_notValidated_failed() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) @@ -261,7 +266,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysShowDataRatIcon_configHasTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this) @@ -277,7 +282,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysShowDataRatIcon_configHasFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this) @@ -293,7 +298,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysUseCdmaLevel_configHasTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this) @@ -309,7 +314,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysUseCdmaLevel_configHasFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Boolean? = null val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this) @@ -323,8 +328,286 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun `default mobile connectivity - uses repo value`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + var expected = MobileConnectivityModel(isConnected = true, isValidated = true) + connectionsRepository.setMobileConnectivity(expected) + assertThat(latest).isEqualTo(expected) + + expected = MobileConnectivityModel(isConnected = false, isValidated = true) + connectionsRepository.setMobileConnectivity(expected) + assertThat(latest).isEqualTo(expected) + + expected = MobileConnectivityModel(isConnected = true, isValidated = false) + connectionsRepository.setMobileConnectivity(expected) + assertThat(latest).isEqualTo(expected) + + expected = MobileConnectivityModel(isConnected = false, isValidated = false) + connectionsRepository.setMobileConnectivity(expected) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun `data switch - in same group - validated matches previous value`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = true, + ) + ) + // Trigger a data change in the same subscription group + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = true, + ) + ) + + job.cancel() + } + + @Test + fun `data switch - in same group - validated matches previous value - expires after 2s`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = true, + ) + ) + // Trigger a data change in the same subscription group + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + // After 1s, the force validation bit is still present + advanceTimeBy(1000) + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = true, + ) + ) + + // After 2s, the force validation expires + advanceTimeBy(1001) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + job.cancel() + } + + @Test + fun `data switch - in same group - not validated - uses new value immediately`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = false, + ) + ) + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + job.cancel() + } + + @Test + fun `data switch - lose validation - then switch happens - clears forced bit`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + // GIVEN the network starts validated + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = true, + ) + ) + + // WHEN a data change happens in the same group + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + + // WHEN the validation bit is lost + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + // WHEN another data change happens in the same group + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + + // THEN the forced validation bit is still removed after 2s + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = true, + ) + ) + + advanceTimeBy(1000) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = true, + ) + ) + + advanceTimeBy(1001) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + job.cancel() + } + + @Test + fun `data switch - while already forcing validation - resets clock`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = true, + ) + ) + + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + + advanceTimeBy(1000) + + // WHEN another change in same group event happens + connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + // THEN the forced validation remains for exactly 2 more seconds from now + + // 1.500s from second event + advanceTimeBy(1500) + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = true, + ) + ) + + // 2.001s from the second event + advanceTimeBy(501) + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + job.cancel() + } + + @Test + fun `data switch - not in same group - uses new values`() = + testScope.runTest { + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = true, + isValidated = true, + ) + ) + connectionsRepository.setMobileConnectivity( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + assertThat(latest) + .isEqualTo( + MobileConnectivityModel( + isConnected = false, + isValidated = false, + ) + ) + + job.cancel() + } + companion object { - private val IMMEDIATE = Dispatchers.Main.immediate private val tableLogBuffer = TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 2a8d42ff6997..a24e29aebc1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -274,6 +274,41 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + fun `network type - alwaysShow - shown when not connected`() = + testScope.runTest { + interactor.setIconGroup(THREE_G) + interactor.isConnected.value = false + interactor.alwaysShowDataRatIcon.value = true + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun `network type - not shown when not connected`() = + testScope.runTest { + interactor.setIconGroup(THREE_G) + interactor.isDataConnected.value = true + interactor.isConnected.value = false + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test fun roaming() = testScope.runTest { interactor.isRoaming.value = true diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt index 30ac8d432e8a..824cebdc3c08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -44,9 +45,53 @@ class WifiNetworkModelTest : SysuiTestCase() { WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1) } + @Test(expected = IllegalArgumentException::class) + fun carrierMerged_invalidSubId_exceptionThrown() { + WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1) + } + // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken @Test + fun logDiffs_carrierMergedToInactive_resetsAllFields() { + val logger = TestLogger() + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) + + WifiNetworkModel.Inactive.logDiffs(prevVal, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + + @Test + fun logDiffs_inactiveToCarrierMerged_logsAllFields() { + val logger = TestLogger() + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) + + carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + + @Test fun logDiffs_inactiveToActive_logsAllActiveFields() { val logger = TestLogger() val activeNetwork = @@ -95,8 +140,14 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) - activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger) + activeNetwork.logDiffs(prevVal, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE)) assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5")) @@ -105,7 +156,7 @@ class WifiNetworkModelTest : SysuiTestCase() { assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID")) } @Test - fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() { + fun logDiffs_activeToCarrierMerged_logsAllFields() { val logger = TestLogger() val activeNetwork = WifiNetworkModel.Active( @@ -114,13 +165,20 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) - WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger) + carrierMerged.logDiffs(prevVal = activeNetwork, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) - assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) - assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) - assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) assertThat(logger.changes).contains(Pair(COL_SSID, "null")) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 8f07615b19b2..87ce8faff5a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -26,6 +26,7 @@ import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher @@ -340,7 +341,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { .launchIn(this) val wifiInfo = mock<WifiInfo>().apply { - whenever(this.ssid).thenReturn(SSID) whenever(this.isPrimary).thenReturn(true) whenever(this.isCarrierMerged).thenReturn(true) } @@ -353,6 +353,67 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java) + + job.cancel() + } + + @Test + fun wifiNetwork_isCarrierMerged_getsCorrectValues() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val rssi = -57 + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.rssi).thenReturn(rssi) + whenever(this.subscriptionId).thenReturn(567) + } + + whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2) + whenever(wifiManager.maxSignalLevel).thenReturn(5) + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged + assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID) + assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567) + assertThat(latestCarrierMerged.level).isEqualTo(2) + // numberOfLevels = maxSignalLevel + 1 + assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6) + + job.cancel() + } + + @Test fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 01d59f96c221..089a170aa2be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -84,7 +84,9 @@ class WifiInteractorImplTest : SysuiTestCase() { @Test fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1) + ) var latest: String? = "default" val job = underTest 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 726e813ec414..b9328377772a 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 @@ -206,7 +206,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // Enabled = false => no networks shown TestCase( enabled = false, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -228,7 +229,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // forceHidden = true => no networks shown TestCase( forceHidden = true, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -369,7 +371,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // network = CarrierMerged => not shown TestCase( - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 4b32ee262cdc..0cca7b2aa38c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -390,19 +390,27 @@ public class RemoteInputViewTest extends SysuiTestCase { bindController(view, row.getEntry()); view.setVisibility(View.GONE); - View crossFadeView = new View(mContext); + View fadeOutView = new View(mContext); + fadeOutView.setId(com.android.internal.R.id.actions_container_layout); - // Start focus animation - view.focusAnimated(crossFadeView); + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeOutView); + // Start focus animation + view.focusAnimated(); assertTrue(view.isAnimatingAppearance()); + // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f + mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1); + assertEquals(0f, fadeOutView.getAlpha()); + // fast forward to end of animation - mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); + mAnimatorTestRule.advanceTimeBy(1); - // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind + // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind // RemoteInputView) - assertEquals(1f, crossFadeView.getAlpha()); + assertEquals(1f, fadeOutView.getAlpha()); assertFalse(view.isAnimatingAppearance()); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(1f, view.getAlpha()); @@ -415,20 +423,27 @@ public class RemoteInputViewTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); ExpandableNotificationRow row = helper.createRow(); - FrameLayout remoteInputViewParent = new FrameLayout(mContext); RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); - remoteInputViewParent.addView(view); bindController(view, row.getEntry()); + View fadeInView = new View(mContext); + fadeInView.setId(com.android.internal.R.id.actions_container_layout); + + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeInView); + // Start defocus animation - view.onDefocus(true, false); + view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */); assertEquals(View.VISIBLE, view.getVisibility()); + assertEquals(0f, fadeInView.getAlpha()); // fast forward to end of animation mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); // assert that RemoteInputView is no longer visible assertEquals(View.GONE, view.getVisibility()); + assertEquals(1f, fadeInView.getAlpha()); } // NOTE: because we're refactoring the RemoteInputView and moving logic into the diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt new file mode 100644 index 000000000000..7e010886c394 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.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.stylus + +import android.hardware.BatteryState + +class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { + override fun getCapacity() = capacity + override fun getStatus() = 0 + override fun isPresent() = true +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt deleted file mode 100644 index 8dd088f5760c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.testing.AndroidTestingRunner -import android.view.InputDevice -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@Ignore("TODO(b/20579491): unignore on main") -class StylusFirstUsageListenerTest : SysuiTestCase() { - @Mock lateinit var context: Context - @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusManager: StylusManager - @Mock lateinit var featureFlags: FeatureFlags - @Mock lateinit var internalStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - @Mock lateinit var externalStylusDevice: InputDevice - @Mock lateinit var batteryState: BatteryState - @Mock lateinit var handler: Handler - - private lateinit var stylusListener: StylusFirstUsageListener - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(false) - - stylusListener = - StylusFirstUsageListener( - context, - inputManager, - stylusManager, - featureFlags, - EXECUTOR, - handler - ) - stylusListener.hasStarted = false - - whenever(handler.post(any())).thenAnswer { - (it.arguments[0] as Runnable).run() - true - } - - whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) - whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(internalStylusDevice.isExternal).thenReturn(false) - whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(externalStylusDevice.isExternal).thenReturn(true) - - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) - whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) - whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(internalStylusDevice) - whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(externalStylusDevice) - } - - @Test - fun start_flagDisabled_doesNotRegister() { - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_toggleHasStarted() { - stylusListener.start() - - assert(stylusListener.hasStarted) - } - - @Test - fun start_hasStarted_doesNotRegister() { - stylusListener.hasStarted = true - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - } - - @Test - fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_stylusEverUsed_doesNotRegister() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(true) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_hostDeviceSupportsStylus_registersListener() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun onStylusAdded_hasNotStarted_doesNotRegisterListener() { - stylusListener.hasStarted = false - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusAdded_internalStylus_registersListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener) - } - - @Test - fun onStylusAdded_externalStylus_doesNotRegisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusAdded_otherDevice_doesNotRegisterListener() { - stylusListener.onStylusAdded(OTHER_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusRemoved_registeredDevice_unregistersListener() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyNoMoreInteractions(inputManager) - } - - @Test - fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onStylusBluetoothConnected_hasNotStarted_doesNoting() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - @Test - fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(true) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(stylusManager) - verify(inputManager, never()) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onBatteryStateChanged_hasNotStarted_doesNothing() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - companion object { - private const val OTHER_DEVICE_ID = 0 - private const val INTERNAL_STYLUS_DEVICE_ID = 1 - private const val EXTERNAL_STYLUS_DEVICE_ID = 2 - private val EXECUTOR = FakeExecutor(FakeSystemClock()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index 984de5b67bf5..6d6e40a90fa6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -17,12 +17,15 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import java.util.concurrent.Executor @@ -31,30 +34,27 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest -@Ignore("b/257936830 until bt APIs") class StylusManagerTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusDevice: InputDevice - @Mock lateinit var btStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - + @Mock lateinit var batteryState: BatteryState @Mock lateinit var bluetoothAdapter: BluetoothAdapter - @Mock lateinit var bluetoothDevice: BluetoothDevice - @Mock lateinit var handler: Handler + @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var stylusCallback: StylusManager.StylusCallback @@ -75,11 +75,8 @@ class StylusManagerTest : SysuiTestCase() { true } - stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR) - - stylusManager.registerCallback(stylusCallback) - - stylusManager.registerBatteryCallback(stylusBatteryCallback) + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) @@ -92,19 +89,47 @@ class StylusManagerTest : SysuiTestCase() { whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID)) + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false) whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice) whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS) + + whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) + + stylusManager.startListener() + stylusManager.registerCallback(stylusCallback) + stylusManager.registerBatteryCallback(stylusBatteryCallback) + clearInvocations(inputManager) } @Test - fun startListener_registersInputDeviceListener() { + fun startListener_hasNotStarted_registersInputDeviceListener() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.startListener() verify(inputManager, times(1)).registerInputDeviceListener(any(), any()) } @Test + fun startListener_hasStarted_doesNothing() { + stylusManager.startListener() + + verifyZeroInteractions(inputManager) + } + + @Test + fun onInputDeviceAdded_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() { stylusManager.registerCallback(otherStylusCallback) @@ -117,6 +142,26 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceAdded_internalStylus_registersBatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(false) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test + fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(true) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, never()) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -125,6 +170,23 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(inputManager, times(1)).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -143,6 +205,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceChanged_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -157,6 +230,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -168,6 +242,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) // whenever(btStylusDevice.bluetoothAddress).thenReturn(null) @@ -179,6 +254,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -189,6 +265,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -198,6 +275,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) stylusManager.registerCallback(otherStylusCallback) @@ -219,6 +307,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_unregistersBatteryListener() { + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceRemoved_btStylus_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -232,6 +331,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_registersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -239,6 +339,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() { whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null) @@ -248,6 +349,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothDisconnected_unregistersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -257,6 +359,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) stylusManager.registerBatteryCallback(otherStylusBatteryCallback) @@ -274,6 +377,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -288,6 +392,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -302,6 +407,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() { stylusManager.onMetadataChanged( bluetoothDevice, @@ -313,6 +419,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -326,6 +433,63 @@ class StylusManagerTest : SysuiTestCase() { .onStylusBluetoothChargingStateChanged(any(), any(), any()) } + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(false) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + fun onBatteryStateChanged_hasNotStarted_doesNothing() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verifyZeroInteractions(inputManager) + } + + @Test + fun onBatteryStateChanged_executesBatteryCallbacks() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusBatteryCallback, times(1)) + .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + } + companion object { private val EXECUTOR = Executor { r -> r.run() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index ff382a3ec19f..1cccd65c8dbc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -25,17 +25,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.whenever -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock -import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -60,7 +58,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { inputManager, stylusUsiPowerUi, featureFlags, - DIRECT_EXECUTOR, ) whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true) @@ -79,40 +76,19 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun start_addsBatteryListenerForInternalStylus() { - startable.start() - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID)) - @Test - fun onStylusAdded_internalStylus_addsBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) + startable.start() - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) + verifyZeroInteractions(stylusManager) } @Test - fun onStylusAdded_externalStylus_doesNotAddBatteryListener() { - startable.onStylusAdded(EXTERNAL_DEVICE_ID) - - verify(inputManager, never()) - .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_initStylusUsiPowerUi() { + startable.start() - @Test - fun onStylusRemoved_registeredStylus_removesBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - startable.onStylusRemoved(STYLUS_DEVICE_ID) - - inOrder(inputManager).let { - it.verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - it.verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable) - } + verify(stylusUsiPowerUi, times(1)).init() } @Test @@ -130,28 +106,34 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun onBatteryStateChanged_batteryPresent_refreshesNotification() { - val batteryState = mock(BatteryState::class.java) - whenever(batteryState.isPresent).thenReturn(true) + fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() { + val batteryState = FixedCapacityBatteryState(0.1f) + + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + + verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState) + } - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + @Test + fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() { + val batteryState = FixedCapacityBatteryState(0f) + + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) - verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState) + verifyNoMoreInteractions(stylusUsiPowerUi) } @Test - fun onBatteryStateChanged_batteryNotPresent_noop() { + fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(false) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verifyNoMoreInteractions(stylusUsiPowerUi) } companion object { - private val DIRECT_EXECUTOR = Executor { r -> r.run() } - private const val EXTERNAL_DEVICE_ID = 0 private const val STYLUS_DEVICE_ID = 1 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt index 59875507341d..1e81dc761b18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt @@ -16,8 +16,12 @@ package com.android.systemui.stylus -import android.hardware.BatteryState +import android.app.Notification +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice @@ -26,14 +30,22 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.doNothing import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -46,13 +58,19 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager @Mock lateinit var handler: Handler @Mock lateinit var btStylusDevice: InputDevice + @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification> private lateinit var stylusUsiPowerUi: StylusUsiPowerUI + private lateinit var broadcastReceiver: BroadcastReceiver + private lateinit var contextSpy: Context @Before fun setUp() { MockitoAnnotations.initMocks(this) + contextSpy = spy(mContext) + doNothing().whenever(contextSpy).startActivity(any()) + whenever(handler.post(any())).thenAnswer { (it.arguments[0] as Runnable).run() true @@ -63,56 +81,77 @@ class StylusUsiPowerUiTest : SysuiTestCase() { whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) // whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES") - stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler) + stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler) + broadcastReceiver = stylusUsiPowerUi.receiver + } + + @Test + fun updateBatteryState_capacityZero_noop() { + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f)) + + verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityBelowThreshold_notifies() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) - verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @Test fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f)) - - verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any()) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f)) + + verify(notificationManager, times(2)) + .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), + context.getString(R.string.stylus_battery_low_percentage, "15%") + ) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), + context.getString(R.string.stylus_battery_low_subtitle) + ) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) it.verifyNoMoreInteractions() } } @@ -121,47 +160,66 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateSuppression_noExistingNotification_cancelsNotification() { stylusUsiPowerUi.updateSuppression(true) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @Test fun updateSuppression_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateSuppression(true) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @Test @Ignore("TODO(b/257936830): get bt address once input api available") - fun refresh_hasConnectedBluetoothStylus_doesNotNotify() { + fun refresh_hasConnectedBluetoothStylus_cancelsNotification() { whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() - verifyNoMoreInteractions(notificationManager) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } @Test @Ignore("TODO(b/257936830): get bt address once input api available") fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() - verify(notificationManager).cancel(R.string.stylus_battery_low) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) + } + + @Test + fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + val activityIntentCaptor = argumentCaptor<Intent>() + stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f)) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture()) + assertThat(activityIntentCaptor.value.action) + .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS) + val args = + activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS) + as Bundle + assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1) } - class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { - override fun getCapacity() = capacity - override fun getStatus() = 0 - override fun isPresent() = true + @Test + fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, never()).startActivity(any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 2a93ffff87a5..d53e09d19048 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -790,15 +790,15 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { reset(mResources); when(mResources.getColor(eq(android.R.color.system_accent1_500), any())) - .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().get(6)); + .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().getS500()); when(mResources.getColor(eq(android.R.color.system_accent2_500), any())) - .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().get(6)); + .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().getS500()); when(mResources.getColor(eq(android.R.color.system_accent3_500), any())) - .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().get(6)); + .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().getS500()); when(mResources.getColor(eq(android.R.color.system_neutral1_500), any())) - .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().get(6)); + .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().getS500()); when(mResources.getColor(eq(android.R.color.system_neutral2_500), any())) - .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().get(6)); + .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().getS500()); // Defers event because we already have initial colors. verify(mThemeOverlayApplier, never()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt index 89402de792dc..f4226bcd71c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.shade.NotificationPanelViewController +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.unfold.util.FoldableDeviceStates @@ -73,6 +74,8 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { @Mock lateinit var viewTreeObserver: ViewTreeObserver + @Mock private lateinit var commandQueue: CommandQueue + @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener> private lateinit var deviceStates: FoldableDeviceStates @@ -102,7 +105,8 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { } keyguardRepository = FakeKeyguardRepository() - val keyguardInteractor = KeyguardInteractor(repository = keyguardRepository) + val keyguardInteractor = + KeyguardInteractor(repository = keyguardRepository, commandQueue = commandQueue) // Needs to be run on the main thread runBlocking(IMMEDIATE) { @@ -171,8 +175,10 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { fold() underTest.onScreenTurningOn({}) - underTest.onStartedWakingUp() + // The body of onScreenTurningOn is executed on fakeExecutor, + // run all pending tasks before calling the next method fakeExecutor.runAllReady() + underTest.onStartedWakingUp() verify(latencyTracker).onActionStart(any()) verify(latencyTracker).onActionCancel(any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt index c31640279305..4a28cd1de255 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt @@ -26,6 +26,10 @@ class TestUnfoldTransitionProvider : UnfoldTransitionProgressProvider, Transitio listeners.forEach { it.onTransitionFinished() } } + override fun onTransitionFinishing() { + listeners.forEach { it.onTransitionFinishing() } + } + override fun onTransitionProgress(progress: Float) { listeners.forEach { it.onTransitionProgress(progress) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt new file mode 100644 index 000000000000..d3fdbd94a5ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.unfold + +import android.os.VibrationEffect +import android.os.Vibrator +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class UnfoldHapticsPlayerTest : SysuiTestCase() { + + private val progressProvider = TestUnfoldTransitionProvider() + private val vibrator: Vibrator = mock() + private val testFoldProvider = TestFoldProvider() + + private lateinit var player: UnfoldHapticsPlayer + + @Before + fun before() { + player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, Runnable::run, vibrator) + } + + @Test + fun testUnfoldingTransitionFinishingEarly_playsHaptics() { + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinishing() + + verify(vibrator).vibrate(any<VibrationEffect>()) + } + + @Test + fun testUnfoldingTransitionFinishingLate_doesNotPlayHaptics() { + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.99f) + progressProvider.onTransitionFinishing() + + verify(vibrator, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun testFoldingAfterUnfolding_doesNotPlayHaptics() { + // Unfold + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinishing() + progressProvider.onTransitionFinished() + clearInvocations(vibrator) + + // Fold + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinished() + testFoldProvider.onFoldUpdate(isFolded = true) + + verify(vibrator, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun testUnfoldingAfterFoldingAndUnfolding_playsHaptics() { + // Unfold + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinishing() + progressProvider.onTransitionFinished() + + // Fold + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinished() + testFoldProvider.onFoldUpdate(isFolded = true) + clearInvocations(vibrator) + + // Unfold again + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinishing() + progressProvider.onTransitionFinished() + + verify(vibrator).vibrate(any<VibrationEffect>()) + } + + private class TestFoldProvider : FoldProvider { + private val listeners = arrayListOf<FoldProvider.FoldCallback>() + + override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) { + listeners += callback + } + + override fun unregisterCallback(callback: FoldProvider.FoldCallback) { + listeners -= callback + } + + fun onFoldUpdate(isFolded: Boolean) { + listeners.forEach { it.onFoldUpdated(isFolded) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 78b0cbe8c718..9bb52be276f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -36,12 +36,14 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -60,12 +62,12 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -73,10 +75,12 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class UserInteractorTest : SysuiTestCase() { @@ -90,10 +94,11 @@ class UserInteractorTest : SysuiTestCase() { @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: UserInteractor - private lateinit var testCoroutineScope: TestCoroutineScope + private lateinit var testScope: TestScope private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var telephonyRepository: FakeTelephonyRepository @@ -117,11 +122,12 @@ class UserInteractorTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() telephonyRepository = FakeTelephonyRepository() - testCoroutineScope = TestCoroutineScope() + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) val refreshUsersScheduler = RefreshUsersScheduler( - applicationScope = testCoroutineScope, - mainDispatcher = IMMEDIATE, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, repository = userRepository, ) underTest = @@ -132,23 +138,24 @@ class UserInteractorTest : SysuiTestCase() { keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, + commandQueue = commandQueue, ), manager = manager, - applicationScope = testCoroutineScope, + applicationScope = testScope.backgroundScope, telephonyInteractor = TelephonyInteractor( repository = telephonyRepository, ), broadcastDispatcher = fakeBroadcastDispatcher, - backgroundDispatcher = IMMEDIATE, + backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = GuestUserInteractor( applicationContext = context, - applicationScope = testCoroutineScope, - mainDispatcher = IMMEDIATE, - backgroundDispatcher = IMMEDIATE, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, manager = manager, repository = userRepository, deviceProvisionedController = deviceProvisionedController, @@ -164,7 +171,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `onRecordSelected - user`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -179,7 +186,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `onRecordSelected - switch to guest user`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -193,7 +200,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `onRecordSelected - enter guest mode`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -202,6 +209,7 @@ class UserInteractorTest : SysuiTestCase() { whenever(manager.createGuest(any())).thenReturn(guestUserInfo) underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) + runCurrent() verify(dialogShower).dismiss() verify(manager).createGuest(any()) @@ -210,7 +218,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `onRecordSelected - action`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -224,81 +232,72 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `users - switcher enabled`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 3, includeGuest = true) + val value = collectLastValue(underTest.users) - job.cancel() + assertUsers(models = value(), count = 3, includeGuest = true) } @Test fun `users - switches to second user`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.users) userRepository.setSelectedUserInfo(userInfos[1]) - assertUsers(models = value, count = 2, selectedIndex = 1) - job.cancel() + assertUsers(models = value(), count = 2, selectedIndex = 1) } @Test fun `users - switcher not enabled`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 1) - - job.cancel() + val value = collectLastValue(underTest.users) + assertUsers(models = value(), count = 1) } @Test fun selectedUser() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var value: UserModel? = null - val job = underTest.selectedUser.onEach { value = it }.launchIn(this) - assertUser(value, id = 0, isSelected = true) + val value = collectLastValue(underTest.selectedUser) + assertUser(value(), id = 0, isSelected = true) userRepository.setSelectedUserInfo(userInfos[1]) - assertUser(value, id = 1, isSelected = true) - - job.cancel() + assertUser(value(), id = 1, isSelected = true) } @Test fun `actions - device unlocked`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) + + runCurrent() - assertThat(value) + assertThat(value()) .isEqualTo( listOf( UserActionModel.ENTER_GUEST_MODE, @@ -307,13 +306,11 @@ class UserInteractorTest : SysuiTestCase() { UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) ) - - job.cancel() } @Test fun `actions - device unlocked - full screen`() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) @@ -321,10 +318,9 @@ class UserInteractorTest : SysuiTestCase() { userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) - assertThat(value) + assertThat(value()) .isEqualTo( listOf( UserActionModel.ADD_USER, @@ -333,46 +329,38 @@ class UserInteractorTest : SysuiTestCase() { UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) ) - - job.cancel() } @Test fun `actions - device unlocked user not primary - empty list`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) - assertThat(value).isEqualTo(emptyList<UserActionModel>()) - - job.cancel() + assertThat(value()).isEqualTo(emptyList<UserActionModel>()) } @Test fun `actions - device unlocked user is guest - empty list`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = true) assertThat(userInfos[1].isGuest).isTrue() userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(emptyList<UserActionModel>()) + val value = collectLastValue(underTest.actions) - job.cancel() + assertThat(value()).isEqualTo(emptyList<UserActionModel>()) } @Test fun `actions - device locked add from lockscreen set - full list`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -383,10 +371,9 @@ class UserInteractorTest : SysuiTestCase() { ) ) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) - assertThat(value) + assertThat(value()) .isEqualTo( listOf( UserActionModel.ENTER_GUEST_MODE, @@ -395,13 +382,11 @@ class UserInteractorTest : SysuiTestCase() { UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) ) - - job.cancel() } @Test fun `actions - device locked add from lockscreen set - full list - full screen`() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) @@ -413,10 +398,9 @@ class UserInteractorTest : SysuiTestCase() { ) ) keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) - assertThat(value) + assertThat(value()) .isEqualTo( listOf( UserActionModel.ADD_USER, @@ -425,39 +409,33 @@ class UserInteractorTest : SysuiTestCase() { UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) ) - - job.cancel() } @Test fun `actions - device locked - only manage user is shown`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) keyguardRepository.setKeyguardShowing(true) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) + val value = collectLastValue(underTest.actions) - assertThat(value).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)) - - job.cancel() + assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)) } @Test fun `executeAction - add user - dialog shown`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) keyguardRepository.setKeyguardShowing(false) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) val dialogShower: UserSwitchDialogController.DialogShower = mock() underTest.executeAction(UserActionModel.ADD_USER, dialogShower) - assertThat(dialogRequest) + assertThat(dialogRequest()) .isEqualTo( ShowDialogRequestModel.ShowAddUserDialog( userHandle = userInfos[0].userHandle, @@ -468,14 +446,12 @@ class UserInteractorTest : SysuiTestCase() { ) underTest.onDialogShown() - assertThat(dialogRequest).isNull() - - job.cancel() + assertThat(dialogRequest()).isNull() } @Test - fun `executeAction - add supervised user - starts activity`() = - runBlocking(IMMEDIATE) { + fun `executeAction - add supervised user - dismisses dialog and starts activity`() = + testScope.runTest { underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) val intentCaptor = kotlinArgumentCaptor<Intent>() @@ -487,7 +463,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `executeAction - navigate to manage users`() = - runBlocking(IMMEDIATE) { + testScope.runTest { underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) val intentCaptor = kotlinArgumentCaptor<Intent>() @@ -497,7 +473,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `executeAction - guest mode`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -505,110 +481,103 @@ class UserInteractorTest : SysuiTestCase() { val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) whenever(manager.createGuest(any())).thenReturn(guestUserInfo) val dialogRequests = mutableListOf<ShowDialogRequestModel?>() - val showDialogsJob = - underTest.dialogShowRequests - .onEach { - dialogRequests.add(it) - if (it != null) { - underTest.onDialogShown() - } + backgroundScope.launch { + underTest.dialogShowRequests.collect { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() } - .launchIn(this) - val dismissDialogsJob = - underTest.dialogDismissRequests - .onEach { - if (it != null) { - underTest.onDialogDismissed() - } + } + } + backgroundScope.launch { + underTest.dialogDismissRequests.collect { + if (it != null) { + underTest.onDialogDismissed() } - .launchIn(this) + } + } underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + runCurrent() assertThat(dialogRequests) .contains( ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), ) verify(activityManager).switchUser(guestUserInfo.id) - - showDialogsJob.cancel() - dismissDialogsJob.cancel() } @Test fun `selectUser - already selected guest re-selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = true) val guestUserInfo = userInfos[1] assertThat(guestUserInfo.isGuest).isTrue() userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(guestUserInfo) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) underTest.selectUser( newlySelectedUserId = guestUserInfo.id, dialogShower = dialogShower, ) - assertThat(dialogRequest) + assertThat(dialogRequest()) .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) verify(dialogShower, never()).dismiss() - job.cancel() } @Test fun `selectUser - currently guest non-guest selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = true) val guestUserInfo = userInfos[1] assertThat(guestUserInfo.isGuest).isTrue() userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(guestUserInfo) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) - assertThat(dialogRequest) + assertThat(dialogRequest()) .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) verify(dialogShower, never()).dismiss() - job.cancel() } @Test fun `selectUser - not currently guest - switches users`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) - assertThat(dialogRequest).isNull() + assertThat(dialogRequest()).isNull() verify(activityManager).switchUser(userInfos[1].id) verify(dialogShower).dismiss() - job.cancel() } @Test fun `Telephony call state changes - refreshes users`() = - runBlocking(IMMEDIATE) { + testScope.runTest { + runCurrent() + val refreshUsersCallCount = userRepository.refreshUsersCallCount telephonyRepository.setCallState(1) + runCurrent() assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) } @Test fun `User switched broadcast`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -617,9 +586,11 @@ class UserInteractorTest : SysuiTestCase() { val callback2: UserInteractor.UserCallback = mock() underTest.addCallback(callback1) underTest.addCallback(callback2) + runCurrent() val refreshUsersCallCount = userRepository.refreshUsersCallCount userRepository.setSelectedUserInfo(userInfos[1]) + runCurrent() fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive( context, @@ -627,16 +598,17 @@ class UserInteractorTest : SysuiTestCase() { .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), ) } + runCurrent() - verify(callback1).onUserStateChanged() - verify(callback2).onUserStateChanged() + verify(callback1, atLeastOnce()).onUserStateChanged() + verify(callback2, atLeastOnce()).onUserStateChanged() assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) } @Test fun `User info changed broadcast`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -649,12 +621,14 @@ class UserInteractorTest : SysuiTestCase() { ) } + runCurrent() + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) } @Test fun `System user unlocked broadcast - refresh users`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -667,13 +641,14 @@ class UserInteractorTest : SysuiTestCase() { .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), ) } + runCurrent() assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) } @Test fun `Non-system user unlocked broadcast - do not refresh users`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -691,14 +666,14 @@ class UserInteractorTest : SysuiTestCase() { @Test fun userRecords() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) keyguardRepository.setKeyguardShowing(false) - testCoroutineScope.advanceUntilIdle() + runCurrent() assertRecords( records = underTest.userRecords.value, @@ -717,7 +692,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun userRecordsFullScreen() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) @@ -725,7 +700,7 @@ class UserInteractorTest : SysuiTestCase() { userRepository.setSelectedUserInfo(userInfos[0]) keyguardRepository.setKeyguardShowing(false) - testCoroutineScope.advanceUntilIdle() + runCurrent() assertRecords( records = underTest.userRecords.value, @@ -744,7 +719,7 @@ class UserInteractorTest : SysuiTestCase() { @Test fun selectedUserRecord() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) userRepository.setUserInfos(userInfos) @@ -762,63 +737,54 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `users - secondary user - guest user can be switched to`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var res: List<UserModel>? = null - val job = underTest.users.onEach { res = it }.launchIn(this) - assertThat(res?.size == 3).isTrue() - assertThat(res?.find { it.isGuest }).isNotNull() - job.cancel() + val res = collectLastValue(underTest.users) + assertThat(res()?.size == 3).isTrue() + assertThat(res()?.find { it.isGuest }).isNotNull() } @Test fun `users - secondary user - no guest action`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var res: List<UserActionModel>? = null - val job = underTest.actions.onEach { res = it }.launchIn(this) - assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull() - job.cancel() + val res = collectLastValue(underTest.actions) + assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull() } @Test fun `users - secondary user - no guest user record`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = true) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var res: List<UserRecord>? = null - val job = underTest.userRecords.onEach { res = it }.launchIn(this) - assertThat(res?.find { it.isGuest }).isNull() - job.cancel() + assertThat(underTest.userRecords.value.find { it.isGuest }).isNull() } @Test fun `show user switcher - full screen disabled - shows dialog switcher`() = - runBlocking(IMMEDIATE) { - var dialogRequest: ShowDialogRequestModel? = null + testScope.runTest { val expandable = mock<Expandable>() underTest.showUserSwitcher(context, expandable) - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogRequest = collectLastValue(underTest.dialogShowRequests) // Dialog is shown. - assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog) + assertThat(dialogRequest()) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) underTest.onDialogShown() - assertThat(dialogRequest).isNull() - - job.cancel() + assertThat(dialogRequest()).isNull() } @Test @@ -849,8 +815,8 @@ class UserInteractorTest : SysuiTestCase() { @Test fun `users - secondary user - managed profile is not included`() = - runBlocking(IMMEDIATE) { - var userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() + testScope.runTest { + val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList() userInfos.add( UserInfo( 50, @@ -863,23 +829,19 @@ class UserInteractorTest : SysuiTestCase() { userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var res: List<UserModel>? = null - val job = underTest.users.onEach { res = it }.launchIn(this) - assertThat(res?.size == 3).isTrue() - job.cancel() + val res = collectLastValue(underTest.users) + assertThat(res()?.size == 3).isTrue() } @Test fun `current user is not primary and user switcher is disabled`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[1]) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - var selectedUser: UserModel? = null - val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this) - assertThat(selectedUser).isNotNull() - job.cancel() + val selectedUser = collectLastValue(underTest.selectedUser) + assertThat(selectedUser()).isNotNull() } private fun assertUsers( @@ -1017,7 +979,6 @@ class UserInteractorTest : SysuiTestCase() { } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) private val GUEST_ICON: Drawable = mock() private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 108fa6246e9c..9a4ca5654691 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -75,6 +76,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: StatusBarUserChipViewModel @@ -241,6 +243,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, + commandQueue = commandQueue, ), featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 784a26bb371b..3d4bbdb23686 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -76,6 +77,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + @Mock private lateinit var commandQueue: CommandQueue private lateinit var underTest: UserSwitcherViewModel @@ -142,6 +144,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, + commandQueue = commandQueue, ), featureFlags = FakeFeatureFlags().apply { @@ -169,273 +172,295 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test - fun users() = testScope.runTest { - val userInfos = - listOf( - UserInfo( - /* id= */ 0, - /* name= */ "zero", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_SYSTEM, - ), - UserInfo( - /* id= */ 1, - /* name= */ "one", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_SYSTEM, - ), - UserInfo( - /* id= */ 2, - /* name= */ "two", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_SYSTEM, - ), - ) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) + fun users() = + testScope.runTest { + val userInfos = + listOf( + UserInfo( + /* id= */ 0, + /* name= */ "zero", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_PRIMARY or + UserInfo.FLAG_ADMIN or + UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 1, + /* name= */ "one", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 2, + /* name= */ "two", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + ) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) - val userViewModels = mutableListOf<List<UserViewModel>>() - val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } + val userViewModels = mutableListOf<List<UserViewModel>>() + val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } - assertThat(userViewModels.last()).hasSize(3) - assertUserViewModel( - viewModel = userViewModels.last()[0], - viewKey = 0, - name = Text.Loaded("zero"), - isSelectionMarkerVisible = true, - ) - assertUserViewModel( - viewModel = userViewModels.last()[1], - viewKey = 1, - name = Text.Loaded("one"), - isSelectionMarkerVisible = false, - ) - assertUserViewModel( - viewModel = userViewModels.last()[2], - viewKey = 2, - name = Text.Loaded("two"), - isSelectionMarkerVisible = false, - ) - job.cancel() - } + assertThat(userViewModels.last()).hasSize(3) + assertUserViewModel( + viewModel = userViewModels.last()[0], + viewKey = 0, + name = Text.Loaded("zero"), + isSelectionMarkerVisible = true, + ) + assertUserViewModel( + viewModel = userViewModels.last()[1], + viewKey = 1, + name = Text.Loaded("one"), + isSelectionMarkerVisible = false, + ) + assertUserViewModel( + viewModel = userViewModels.last()[2], + viewKey = 2, + name = Text.Loaded("two"), + isSelectionMarkerVisible = false, + ) + job.cancel() + } @Test - fun `maximumUserColumns - few users`() = testScope.runTest { - setUsers(count = 2) - val values = mutableListOf<Int>() - val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + fun `maximumUserColumns - few users`() = + testScope.runTest { + setUsers(count = 2) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } - assertThat(values.last()).isEqualTo(4) + assertThat(values.last()).isEqualTo(4) - job.cancel() - } + job.cancel() + } @Test - fun `maximumUserColumns - many users`() = testScope.runTest { - setUsers(count = 5) - val values = mutableListOf<Int>() - val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } - - assertThat(values.last()).isEqualTo(3) - job.cancel() - } + fun `maximumUserColumns - many users`() = + testScope.runTest { + setUsers(count = 5) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + + assertThat(values.last()).isEqualTo(3) + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - has actions - true`() = testScope.runTest { - setUsers(2) + fun `isOpenMenuButtonVisible - has actions - true`() = + testScope.runTest { + setUsers(2) - val isVisible = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - assertThat(isVisible.last()).isTrue() - job.cancel() - } + assertThat(isVisible.last()).isTrue() + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - no actions - false`() = testScope.runTest { - val userInfos = setUsers(2) - userRepository.setSelectedUserInfo(userInfos[1]) - keyguardRepository.setKeyguardShowing(true) - whenever(manager.canAddMoreUsers(any())).thenReturn(false) - - val isVisible = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - - assertThat(isVisible.last()).isFalse() - job.cancel() - } + fun `isOpenMenuButtonVisible - no actions - false`() = + testScope.runTest { + val userInfos = setUsers(2) + userRepository.setSelectedUserInfo(userInfos[1]) + keyguardRepository.setKeyguardShowing(true) + whenever(manager.canAddMoreUsers(any())).thenReturn(false) + + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } + + assertThat(isVisible.last()).isFalse() + job.cancel() + } @Test - fun menu() = testScope.runTest { - val isMenuVisible = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) } - assertThat(isMenuVisible.last()).isFalse() + fun menu() = + testScope.runTest { + val isMenuVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) } + assertThat(isMenuVisible.last()).isFalse() - underTest.onOpenMenuButtonClicked() - assertThat(isMenuVisible.last()).isTrue() + underTest.onOpenMenuButtonClicked() + assertThat(isMenuVisible.last()).isTrue() - underTest.onMenuClosed() - assertThat(isMenuVisible.last()).isFalse() + underTest.onMenuClosed() + assertThat(isMenuVisible.last()).isFalse() - job.cancel() - } + job.cancel() + } @Test - fun `menu actions`() = testScope.runTest { - setUsers(2) - val actions = mutableListOf<List<UserActionViewModel>>() - val job = launch(testDispatcher) { underTest.menu.toList(actions) } - - assertThat(actions.last().map { it.viewKey }) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), - UserActionModel.ADD_USER.ordinal.toLong(), - UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), + fun `menu actions`() = + testScope.runTest { + setUsers(2) + val actions = mutableListOf<List<UserActionViewModel>>() + val job = launch(testDispatcher) { underTest.menu.toList(actions) } + + assertThat(actions.last().map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), + ) ) - ) - job.cancel() - } + job.cancel() + } @Test - fun `isFinishRequested - finishes when user is switched`() = testScope.runTest { - val userInfos = setUsers(count = 2) - val isFinishRequested = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() + fun `isFinishRequested - finishes when user is switched`() = + testScope.runTest { + val userInfos = setUsers(count = 2) + val isFinishRequested = mutableListOf<Boolean>() + val job = + launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() - userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSelectedUserInfo(userInfos[1]) - assertThat(isFinishRequested.last()).isTrue() + assertThat(isFinishRequested.last()).isTrue() - job.cancel() - } + job.cancel() + } @Test - fun `isFinishRequested - finishes when the screen turns off`() = testScope.runTest { - setUsers(count = 2) - powerRepository.setInteractive(true) - val isFinishRequested = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() + fun `isFinishRequested - finishes when the screen turns off`() = + testScope.runTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = + launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() - powerRepository.setInteractive(false) + powerRepository.setInteractive(false) - assertThat(isFinishRequested.last()).isTrue() + assertThat(isFinishRequested.last()).isTrue() - job.cancel() - } + job.cancel() + } @Test - fun `isFinishRequested - finishes when cancel button is clicked`() = testScope.runTest { - setUsers(count = 2) - powerRepository.setInteractive(true) - val isFinishRequested = mutableListOf<Boolean>() - val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() + fun `isFinishRequested - finishes when cancel button is clicked`() = + testScope.runTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = + launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() - underTest.onCancelButtonClicked() + underTest.onCancelButtonClicked() - assertThat(isFinishRequested.last()).isTrue() + assertThat(isFinishRequested.last()).isTrue() - underTest.onFinished() + underTest.onFinished() - assertThat(isFinishRequested.last()).isFalse() + assertThat(isFinishRequested.last()).isFalse() - job.cancel() - } + job.cancel() + } @Test - fun `guest selected -- name is exit guest`() = testScope.runTest { - val userInfos = + fun `guest selected -- name is exit guest`() = + testScope.runTest { + val userInfos = listOf( - UserInfo( - /* id= */ 0, - /* name= */ "zero", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_SYSTEM, - ), - UserInfo( - /* id= */ 1, - /* name= */ "one", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_GUEST, - ), + UserInfo( + /* id= */ 0, + /* name= */ "zero", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_PRIMARY or + UserInfo.FLAG_ADMIN or + UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 1, + /* name= */ "one", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_GUEST, + ), ) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) - val userViewModels = mutableListOf<List<UserViewModel>>() - val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } + val userViewModels = mutableListOf<List<UserViewModel>>() + val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } - assertThat(userViewModels.last()).hasSize(2) - assertUserViewModel( + assertThat(userViewModels.last()).hasSize(2) + assertUserViewModel( viewModel = userViewModels.last()[0], viewKey = 0, name = Text.Loaded("zero"), isSelectionMarkerVisible = false, - ) - assertUserViewModel( + ) + assertUserViewModel( viewModel = userViewModels.last()[1], viewKey = 1, - name = Text.Resource( - com.android.settingslib.R.string.guest_exit_quick_settings_button - ), + name = + Text.Resource( + com.android.settingslib.R.string.guest_exit_quick_settings_button + ), isSelectionMarkerVisible = true, - ) - job.cancel() - } + ) + job.cancel() + } @Test - fun `guest not selected -- name is guest`() = testScope.runTest { - val userInfos = + fun `guest not selected -- name is guest`() = + testScope.runTest { + val userInfos = listOf( - UserInfo( - /* id= */ 0, - /* name= */ "zero", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_SYSTEM, - ), - UserInfo( - /* id= */ 1, - /* name= */ "one", - /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_FULL, - UserManager.USER_TYPE_FULL_GUEST, - ), + UserInfo( + /* id= */ 0, + /* name= */ "zero", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_PRIMARY or + UserInfo.FLAG_ADMIN or + UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 1, + /* name= */ "one", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_FULL, + UserManager.USER_TYPE_FULL_GUEST, + ), ) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - runCurrent() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + runCurrent() - val userViewModels = mutableListOf<List<UserViewModel>>() - val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } + val userViewModels = mutableListOf<List<UserViewModel>>() + val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } - assertThat(userViewModels.last()).hasSize(2) - assertUserViewModel( + assertThat(userViewModels.last()).hasSize(2) + assertUserViewModel( viewModel = userViewModels.last()[0], viewKey = 0, name = Text.Loaded("zero"), isSelectionMarkerVisible = true, - ) - assertUserViewModel( + ) + assertUserViewModel( viewModel = userViewModels.last()[1], viewKey = 1, name = Text.Loaded("one"), isSelectionMarkerVisible = false, - ) - job.cancel() - } + ) + job.cancel() + } private suspend fun setUsers(count: Int): List<UserInfo> { val userInfos = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index c3c6975af870..d419095921b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -16,6 +16,7 @@ package com.android.systemui.volume; +import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; import static junit.framework.Assert.assertEquals; @@ -342,6 +343,15 @@ public class VolumeDialogImplTest extends SysuiTestCase { assertEquals(mDialog.mVolumeRingerMuteIconDrawableId, R.drawable.ic_volume_ringer_mute); } + @Test + public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() { + mDialog.dismissH(DISMISS_REASON_UNKNOWN); + // notifyVisible(false) should not be called immediately but only after the dismiss + // animation has ended. + verify(mVolumeDialogController, times(0)).notifyVisible(false); + mDialog.getDialogView().animate().cancel(); + } + /* @Test public void testContentDescriptions() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt new file mode 100644 index 000000000000..b52786178e71 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wallet.controller + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.service.quickaccesswallet.GetWalletCardsResponse +import android.service.quickaccesswallet.QuickAccessWalletClient +import android.service.quickaccesswallet.WalletCard +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +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 com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import java.util.ArrayList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.isNull +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class WalletContextualSuggestionsControllerTest : SysuiTestCase() { + + @Mock private lateinit var walletController: QuickAccessWalletController + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var featureFlags: FeatureFlags + @Mock private lateinit var mockContext: Context + @Captor private lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver> + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever( + broadcastDispatcher.broadcastFlow<List<String>?>( + any(), + isNull(), + any(), + any(), + any() + ) + ) + .thenCallRealMethod() + + whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) + .thenReturn(true) + + whenever(CARD_1.cardId).thenReturn(ID_1) + whenever(CARD_2.cardId).thenReturn(ID_2) + whenever(CARD_3.cardId).thenReturn(ID_3) + } + + @Test + fun `state - has wallet cards - received contextual cards`() = runTest { + setUpWalletClient(listOf(CARD_1, CARD_2)) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verifyRegistered() + broadcastReceiver.value.onReceive( + mockContext, + createContextualCardsIntent(listOf(ID_1, ID_2)) + ) + + assertThat(latest()).containsExactly(CARD_1, CARD_2) + } + + @Test + fun `state - no wallet cards - received contextual cards`() = runTest { + setUpWalletClient(emptyList()) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verifyRegistered() + broadcastReceiver.value.onReceive( + mockContext, + createContextualCardsIntent(listOf(ID_1, ID_2)) + ) + + assertThat(latest()).isEmpty() + } + + @Test + fun `state - has wallet cards - no contextual cards`() = runTest { + setUpWalletClient(listOf(CARD_1, CARD_2)) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verifyRegistered() + broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList())) + + assertThat(latest()).isEmpty() + } + + @Test + fun `state - wallet cards error`() = runTest { + setUpWalletClient(shouldFail = true) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verifyRegistered() + broadcastReceiver.value.onReceive( + mockContext, + createContextualCardsIntent(listOf(ID_1, ID_2)) + ) + + assertThat(latest()).isEmpty() + } + + @Test + fun `state - no contextual cards extra`() = runTest { + setUpWalletClient(listOf(CARD_1, CARD_2)) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verifyRegistered() + broadcastReceiver.value.onReceive(mockContext, Intent(INTENT_NAME)) + + assertThat(latest()).isEmpty() + } + + @Test + fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest { + whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS))) + .thenReturn(false) + setUpWalletClient(listOf(CARD_1, CARD_2)) + val latest = + collectLastValue( + createWalletContextualSuggestionsController(backgroundScope) + .contextualSuggestionCards, + ) + + runCurrent() + verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any()) + assertThat(latest()).isNull() + } + + private fun createWalletContextualSuggestionsController( + scope: CoroutineScope + ): WalletContextualSuggestionsController { + return WalletContextualSuggestionsController( + scope, + walletController, + broadcastDispatcher, + featureFlags + ) + } + + private fun verifyRegistered() { + verify(broadcastDispatcher) + .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any()) + } + + private fun createContextualCardsIntent( + ids: List<String> = emptyList(), + ): Intent { + val intent = Intent(INTENT_NAME) + intent.putStringArrayListExtra("cardIds", ArrayList(ids)) + return intent + } + + private fun setUpWalletClient( + cards: List<WalletCard> = emptyList(), + shouldFail: Boolean = false + ) { + whenever(walletController.queryWalletCards(any())).thenAnswer { invocation -> + with( + invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback + ) { + if (shouldFail) { + onWalletCardRetrievalError(mock()) + } else { + onWalletCardsRetrieved(GetWalletCardsResponse(cards, 0)) + } + } + } + } + + companion object { + private const val ID_1: String = "123" + private val CARD_1: WalletCard = mock() + private const val ID_2: String = "456" + private val CARD_2: WalletCard = mock() + private const val ID_3: String = "789" + private val CARD_3: WalletCard = mock() + private val INTENT_NAME: String = "WalletSuggestionsIntent" + } +} 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 388c51f04e13..dec80807ec87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -24,6 +24,7 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.google.common.truth.Truth.assertThat; @@ -228,6 +229,8 @@ public class BubblesTest extends SysuiTestCase { private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; + private Intent mAppBubbleIntent; + @Mock private ShellInit mShellInit; @Mock @@ -323,6 +326,9 @@ public class BubblesTest extends SysuiTestCase { mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); + mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + mAppBubbleIntent.setPackage(mContext.getPackageName()); + mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); @@ -1630,6 +1636,62 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } + @Test + public void testShowOrHideAppBubble_addsAndExpand() { + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), + /* showInShade= */ eq(false)); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + } + + @Test + public void testShowOrHideAppBubble_expandIfCollapsed() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.collapseStack(); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + + // Calling this while collapsed will expand the app bubble + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + + @Test + public void testShowOrHideAppBubble_collapseIfSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + // Calling this while the app bubble is expanded should collapse the stack + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); + } + + @Test + public void testShowOrHideAppBubble_selectIfNotSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.expandStackAndSelectBubble(mBubbleEntry); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java index 3767fbe98dc1..342855357fd2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java @@ -40,24 +40,49 @@ import java.io.IOException; public class MemoryTrackingTestCase extends SysuiTestCase { private static File sFilesDir = null; private static String sLatestTestClassName = null; + private static int sHeapCount = 0; + private static File sLatestBaselineHeapFile = null; - @Before public void grabFilesDir() { + // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files + // dir, and that does not exist until @Before on each test method. + @Before + public void grabFilesDir() throws IOException { + // This should happen only once per suite if (sFilesDir == null) { sFilesDir = mContext.getFilesDir(); } - sLatestTestClassName = getClass().getName(); + + // This will happen before the first test method in each class + if (sLatestTestClassName == null) { + sLatestTestClassName = getClass().getName(); + sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test"); + } } @AfterClass public static void dumpHeap() throws IOException { + File afterTestHeap = dump(sLatestTestClassName, "after-test"); + if (sLatestBaselineHeapFile != null && afterTestHeap != null) { + Log.w("MEMORY", "To compare heap to baseline (use go/ahat):"); + Log.w("MEMORY", " adb pull " + sLatestBaselineHeapFile); + Log.w("MEMORY", " adb pull " + afterTestHeap); + Log.w("MEMORY", + " java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " " + + afterTestHeap.getName()); + } + sLatestTestClassName = null; + } + + private static File dump(String basename, String heapKind) throws IOException { if (sFilesDir == null) { Log.e("MEMORY", "Somehow no test cases??"); - return; + return null; } mockitoTearDown(); - Log.w("MEMORY", "about to dump heap"); - File path = new File(sFilesDir, sLatestTestClassName + ".ahprof"); + Log.w("MEMORY", "about to dump " + heapKind + " heap"); + File path = new File(sFilesDir, basename + ".ahprof"); Debug.dumpHprofData(path.getPath()); - Log.w("MEMORY", "did it! Location: " + path); + Log.w("MEMORY", "Success! Location: " + path); + return path; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index bf2235aa98a9..1bab99787de4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -132,8 +132,10 @@ public abstract class SysuiTestCase { @After public void SysuiTeardown() { - InstrumentationRegistry.registerInstance(mRealInstrumentation, - InstrumentationRegistry.getArguments()); + if (mRealInstrumentation != null) { + InstrumentationRegistry.registerInstance(mRealInstrumentation, + InstrumentationRegistry.getArguments()); + } if (TestableLooper.get(this) != null) { TestableLooper.get(this).processAllMessages(); // Must remove static reference to this test object to prevent leak (b/261039202) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt index 8176dd07b84a..1bdee3667d04 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt @@ -19,9 +19,10 @@ package com.android.systemui.biometrics.udfps import android.graphics.Rect class FakeOverlapDetector : OverlapDetector { - var shouldReturn: Boolean = false + var shouldReturn: Map<Int, Boolean> = mapOf() override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { - return shouldReturn + return shouldReturn[touchData.pointerId] + ?: error("Unexpected PointerId not declared in TestCase currentPointers") } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt new file mode 100644 index 000000000000..f3e52de0d7a0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt @@ -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. + * + */ + +package com.android.systemui.keyguard.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeBiometricRepository : BiometricRepository { + + private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false) + override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow() + + private val _isStrongBiometricAllowed = MutableStateFlow(false) + override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow() + + private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false) + override val isFingerprintEnabledByDevicePolicy = + _isFingerprintEnabledByDevicePolicy.asStateFlow() + + fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { + _isFingerprintEnrolled.value = isFingerprintEnrolled + } + + fun setStrongBiometricAllowed(isStrongBiometricAllowed: Boolean) { + _isStrongBiometricAllowed.value = isStrongBiometricAllowed + } + + fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) { + _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy + } +} 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 39d2ecaef51a..15b473640de7 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 @@ -52,6 +52,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing + private val _isAodAvailable = MutableStateFlow(false) + override val isAodAvailable: Flow<Boolean> = _isAodAvailable + private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming @@ -126,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isDozing.value = isDozing } + fun setAodAvailable(isAodAvailable: Boolean) { + _isAodAvailable.value = isAodAvailable + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java index 045e6f19c667..7bcad456ff6e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; + import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; @@ -57,6 +59,7 @@ public class RankingBuilder { private ShortcutInfo mShortcutInfo = null; private int mRankingAdjustment = 0; private boolean mIsBubble = false; + private int mProposedImportance = IMPORTANCE_UNSPECIFIED; public RankingBuilder() { } @@ -86,6 +89,7 @@ public class RankingBuilder { mShortcutInfo = ranking.getConversationShortcutInfo(); mRankingAdjustment = ranking.getRankingAdjustment(); mIsBubble = ranking.isBubble(); + mProposedImportance = ranking.getProposedImportance(); } public Ranking build() { @@ -114,7 +118,8 @@ public class RankingBuilder { mIsConversation, mShortcutInfo, mRankingAdjustment, - mIsBubble); + mIsBubble, + mProposedImportance); return ranking; } @@ -214,6 +219,11 @@ public class RankingBuilder { return this; } + public RankingBuilder setProposedImportance(@Importance int importance) { + mProposedImportance = importance; + return this; + } + public RankingBuilder setUserSentiment(int userSentiment) { mUserSentiment = userSentiment; return this; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java index 2d6d29a50a74..926c6c56a862 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java @@ -98,6 +98,10 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager> } @Override + public void removeAllIconsForExternalSlot(String slot) { + } + + @Override public void setIconAccessibilityLiveRegion(String slot, int mode) { } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt index 5a868a4df354..cfb959e51d4e 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt @@ -22,8 +22,8 @@ import android.hardware.SensorManager import android.os.Handler import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig -import com.android.systemui.unfold.dagger.UnfoldBackground import com.android.systemui.unfold.dagger.UnfoldMain +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider @@ -58,7 +58,7 @@ interface UnfoldSharedComponent { @BindsInstance sensorManager: SensorManager, @BindsInstance @UnfoldMain handler: Handler, @BindsInstance @UnfoldMain executor: Executor, - @BindsInstance @UnfoldBackground backgroundExecutor: Executor, + @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, @BindsInstance windowManager: IWindowManager, @BindsInstance contentResolver: ContentResolver = context.contentResolver diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt index 3fa546914d3a..31616fa54bf4 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt @@ -16,9 +16,7 @@ package com.android.systemui.unfold -import android.hardware.SensorManager import com.android.systemui.unfold.config.UnfoldTransitionConfig -import com.android.systemui.unfold.dagger.UnfoldBackground import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider import com.android.systemui.unfold.updates.DeviceFoldStateProvider @@ -34,55 +32,18 @@ import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider import dagger.Module import dagger.Provides import java.util.Optional -import java.util.concurrent.Executor +import javax.inject.Provider import javax.inject.Singleton -@Module +@Module(includes = [UnfoldSharedInternalModule::class]) class UnfoldSharedModule { @Provides @Singleton - fun unfoldTransitionProgressProvider( - config: UnfoldTransitionConfig, - scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory, - tracingListener: ATraceLoggerTransitionProgressListener, - foldStateProvider: FoldStateProvider - ): Optional<UnfoldTransitionProgressProvider> = - if (!config.isEnabled) { - Optional.empty() - } else { - val baseProgressProvider = - if (config.isHingeAngleEnabled) { - PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider) - } else { - FixedTimingTransitionProgressProvider(foldStateProvider) - } - Optional.of( - scaleAwareProviderFactory.wrap(baseProgressProvider).apply { - // Always present callback that logs animation beginning and end. - addCallback(tracingListener) - } - ) - } - - @Provides - @Singleton fun provideFoldStateProvider( deviceFoldStateProvider: DeviceFoldStateProvider ): FoldStateProvider = deviceFoldStateProvider @Provides - fun hingeAngleProvider( - config: UnfoldTransitionConfig, - sensorManager: SensorManager, - @UnfoldBackground executor: Executor - ): HingeAngleProvider = - if (config.isHingeAngleEnabled) { - HingeSensorAngleProvider(sensorManager, executor) - } else { - EmptyHingeAngleProvider - } - - @Provides @Singleton fun unfoldKeyguardVisibilityProvider( impl: UnfoldKeyguardVisibilityManagerImpl @@ -94,3 +55,51 @@ class UnfoldSharedModule { impl: UnfoldKeyguardVisibilityManagerImpl ): UnfoldKeyguardVisibilityManager = impl } + +/** + * Needed as methods inside must be public, but their parameters can be internal (and, a public + * method can't have internal parameters). Making the module internal and included in a public one + * fixes the issue. + */ +@Module +internal class UnfoldSharedInternalModule { + @Provides + @Singleton + fun unfoldTransitionProgressProvider( + config: UnfoldTransitionConfig, + scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory, + tracingListener: ATraceLoggerTransitionProgressListener, + physicsBasedUnfoldTransitionProgressProvider: + Provider<PhysicsBasedUnfoldTransitionProgressProvider>, + fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>, + ): Optional<UnfoldTransitionProgressProvider> { + if (!config.isEnabled) { + return Optional.empty() + } + val baseProgressProvider = + if (config.isHingeAngleEnabled) { + physicsBasedUnfoldTransitionProgressProvider.get() + } else { + fixedTimingTransitionProgressProvider.get() + } + + return Optional.of( + scaleAwareProviderFactory.wrap(baseProgressProvider).apply { + // Always present callback that logs animation beginning and end. + addCallback(tracingListener) + } + ) + } + + @Provides + fun hingeAngleProvider( + config: UnfoldTransitionConfig, + hingeAngleSensorProvider: Provider<HingeSensorAngleProvider> + ): HingeAngleProvider { + return if (config.isHingeAngleEnabled) { + hingeAngleSensorProvider.get() + } else { + EmptyHingeAngleProvider + } + } +} diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt index a1ed17844e8e..aa93c6290145 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt @@ -37,29 +37,29 @@ import java.util.concurrent.Executor * This should **never** be called from sysui, as the object is already provided in that process. */ fun createUnfoldSharedComponent( - context: Context, - config: UnfoldTransitionConfig, - screenStatusProvider: ScreenStatusProvider, - foldProvider: FoldProvider, - activityTypeProvider: CurrentActivityTypeProvider, - sensorManager: SensorManager, - mainHandler: Handler, - mainExecutor: Executor, - backgroundExecutor: Executor, - tracingTagPrefix: String, - windowManager: IWindowManager, + context: Context, + config: UnfoldTransitionConfig, + screenStatusProvider: ScreenStatusProvider, + foldProvider: FoldProvider, + activityTypeProvider: CurrentActivityTypeProvider, + sensorManager: SensorManager, + mainHandler: Handler, + mainExecutor: Executor, + singleThreadBgExecutor: Executor, + tracingTagPrefix: String, + windowManager: IWindowManager, ): UnfoldSharedComponent = - DaggerUnfoldSharedComponent.factory() - .create( - context, - config, - screenStatusProvider, - foldProvider, - activityTypeProvider, - sensorManager, - mainHandler, - mainExecutor, - backgroundExecutor, - tracingTagPrefix, - windowManager, - ) + DaggerUnfoldSharedComponent.factory() + .create( + context, + config, + screenStatusProvider, + foldProvider, + activityTypeProvider, + sensorManager, + mainHandler, + mainExecutor, + singleThreadBgExecutor, + tracingTagPrefix, + windowManager, + ) diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt index 60747954dac3..dcac531485e3 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt @@ -18,8 +18,7 @@ import javax.inject.Qualifier /** * Alternative to [UiBackground] qualifier annotation in unfold module. + * * It is needed as we can't depend on SystemUI code in this module. */ -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class UnfoldBackground +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnfoldSingleThreadBg diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt index fa59cb4d12be..4622464b204d 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt @@ -24,11 +24,13 @@ import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate +import javax.inject.Inject /** Emits animation progress with fixed timing after unfolding */ -internal class FixedTimingTransitionProgressProvider( - private val foldStateProvider: FoldStateProvider -) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener { +internal class FixedTimingTransitionProgressProvider +@Inject +constructor(private val foldStateProvider: FoldStateProvider) : + UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener { private val animatorListener = AnimatorListener() private val animator = diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index 074b1e162fed..6ffbe5aa25c0 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -33,9 +33,10 @@ import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener import com.android.systemui.unfold.updates.name +import javax.inject.Inject /** Maps fold updates to unfold transition progress using DynamicAnimation. */ -class PhysicsBasedUnfoldTransitionProgressProvider( +class PhysicsBasedUnfoldTransitionProgressProvider @Inject constructor( private val foldStateProvider: FoldStateProvider ) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener { diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 5b458975fa34..97c9ba99f096 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -79,6 +79,7 @@ constructor( screenStatusProvider.addCallback(screenListener) hingeAngleProvider.addCallback(hingeAngleListener) rotationChangeProvider.addCallback(rotationListener) + activityTypeProvider.init() } override fun stop() { @@ -87,6 +88,7 @@ constructor( hingeAngleProvider.removeCallback(hingeAngleListener) hingeAngleProvider.stop() rotationChangeProvider.removeCallback(rotationListener) + activityTypeProvider.uninit() } override fun addCallback(listener: FoldUpdatesListener) { @@ -115,19 +117,17 @@ constructor( } val isClosing = angle < lastHingeAngle - val closingThreshold = getClosingThreshold() - val closingThresholdMet = closingThreshold == null || angle < closingThreshold val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING val screenAvailableEventSent = isUnfoldHandled if (isClosing // hinge angle should be decreasing since last update - && closingThresholdMet // hinge angle is below certain threshold && !closingEventDispatched // we haven't sent closing event already && !isFullyOpened // do not send closing event if we are in fully opened hinge // angle range as closing threshold could overlap this range && screenAvailableEventSent // do not send closing event if we are still in // the process of turning on the inner display + && isClosingThresholdMet(angle) // hinge angle is below certain threshold. ) { notifyFoldUpdate(FOLD_UPDATE_START_CLOSING) } @@ -146,6 +146,11 @@ constructor( outputListeners.forEach { it.onHingeAngleUpdate(angle) } } + private fun isClosingThresholdMet(currentAngle: Float) : Boolean { + val closingThreshold = getClosingThreshold() + return closingThreshold == null || currentAngle < closingThreshold + } + /** * Fold animation should be started only after the threshold returned here. * diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt index 577137ca12f3..89fb12e313ec 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt @@ -20,35 +20,43 @@ import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.Trace import androidx.core.util.Consumer +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg import java.util.concurrent.Executor +import javax.inject.Inject -internal class HingeSensorAngleProvider( +internal class HingeSensorAngleProvider +@Inject +constructor( private val sensorManager: SensorManager, - private val executor: Executor -) : - HingeAngleProvider { + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor +) : HingeAngleProvider { private val sensorListener = HingeAngleSensorListener() private val listeners: MutableList<Consumer<Float>> = arrayListOf() var started = false - override fun start() = executor.execute { - if (started) return@execute - Trace.beginSection("HingeSensorAngleProvider#start") - val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE) - sensorManager.registerListener( - sensorListener, - sensor, - SensorManager.SENSOR_DELAY_FASTEST - ) - Trace.endSection() - started = true + override fun start() { + singleThreadBgExecutor.execute { + if (started) return@execute + Trace.beginSection("HingeSensorAngleProvider#start") + val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE) + sensorManager.registerListener( + sensorListener, + sensor, + SensorManager.SENSOR_DELAY_FASTEST + ) + Trace.endSection() + + started = true + } } - override fun stop() = executor.execute { - if (!started) return@execute - sensorManager.unregisterListener(sensorListener) - started = false + override fun stop() { + singleThreadBgExecutor.execute { + if (!started) return@execute + sensorManager.unregisterListener(sensorListener) + started = false + } } override fun removeCallback(listener: Consumer<Float>) { diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt index d0e6cdc9a3c6..34e7c38c1e59 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt @@ -16,6 +16,11 @@ package com.android.systemui.unfold.util interface CurrentActivityTypeProvider { val isHomeActivity: Boolean? + + /** Starts listening for task updates. */ + fun init() {} + /** Stop listening for task updates. */ + fun uninit() {} } class EmptyCurrentActivityTypeProvider(override val isHomeActivity: Boolean? = null) : diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 308f36029e02..9d91b9753691 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -817,7 +817,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (host != null) { host.callbacks = null; pruneHostLocked(host); - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -888,12 +889,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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; - } + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -4345,14 +4342,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku PendingHostUpdate.appWidgetRemoved(appWidgetId)); } - public SparseArray<String> getWidgetUids() { + public SparseArray<String> getWidgetUidsIfBound() { final SparseArray<String> uids = new SparseArray<>(); for (int i = widgets.size() - 1; i >= 0; i--) { final Widget widget = widgets.get(i); if (widget.provider == null) { if (DEBUG) { - Slog.e(TAG, "Widget with no provider " + widget.toString()); + Slog.d(TAG, "Widget with no provider " + widget.toString()); } + continue; } final ProviderId providerId = widget.provider.id; uids.put(providerId.uid, providerId.componentName.getPackageName()); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 677871f6c85f..8c2c964e2d2c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -357,6 +357,7 @@ final class SaveUi { params.width = WindowManager.LayoutParams.MATCH_PARENT; params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; + params.setTrustedOverlay(); show(); } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 89447b457c20..f8467410b3ba 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -3091,7 +3091,7 @@ public class AccountManagerService } } - Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + Intent intent = result.getParcelable(AccountManager.KEY_INTENT, Intent.class); if (intent != null && notifyOnAuthFailure && !customTokens) { /* * Make sure that the supplied intent is owned by the authenticator @@ -3516,8 +3516,7 @@ public class AccountManagerService Bundle.setDefusable(result, true); mNumResults++; Intent intent = null; - if (result != null - && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { + if (result != null) { if (!checkKeyIntent( Binder.getCallingUid(), result)) { @@ -4886,8 +4885,10 @@ public class AccountManagerService EventLog.writeEvent(0x534e4554, "250588548", authUid, ""); return false; } - Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class); + if (intent == null) { + return true; + } // Explicitly set an empty ClipData to ensure that we don't offer to // promote any Uris contained inside for granting purposes if (intent.getClipData() == null) { @@ -4937,8 +4938,12 @@ public class AccountManagerService Bundle simulateBundle = p.readBundle(); p.recycle(); Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class); - return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT, - Intent.class))); + Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT, + Intent.class); + if (intent == null) { + return (simulateIntent == null); + } + return intent.filterEquals(simulateIntent); } private boolean isExportedSystemActivity(ActivityInfo activityInfo) { @@ -5087,8 +5092,7 @@ public class AccountManagerService } } } - if (result != null - && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { + if (result != null) { if (!checkKeyIntent( Binder.getCallingUid(), result)) { diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 9669c060b716..c36e0700c723 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -3420,6 +3420,11 @@ public final class ActiveServices { throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + className + " is not an isolatedProcess"); } + if (!mAm.getPackageManagerInternal().isSameApp(callingPackage, callingUid, + userId)) { + throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + + "calling package not owned by calling UID "); + } // Run the service under the calling package's application. ApplicationInfo aInfo = AppGlobals.getPackageManager().getApplicationInfo( callingPackage, ActivityManagerService.STOCK_PM_FLAGS, userId); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 730c4108220a..5789eb14dc78 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -82,6 +82,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private final @NonNull AudioService mAudioService; private final @NonNull Context mContext; + private final @NonNull AudioSystemAdapter mAudioSystem; /** ID for Communication strategy retrieved form audio policy manager */ private int mCommunicationStrategyId = -1; @@ -156,12 +157,14 @@ import java.util.concurrent.atomic.AtomicBoolean; public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L; //------------------------------------------------------------------- - /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) { + /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service, + @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; mBtHelper = new BtHelper(this); mDeviceInventory = new AudioDeviceInventory(this); mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext); + mAudioSystem = audioSystem; init(); } @@ -170,12 +173,14 @@ import java.util.concurrent.atomic.AtomicBoolean; * in system_server */ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service, @NonNull AudioDeviceInventory mockDeviceInventory, - @NonNull SystemServerAdapter mockSystemServer) { + @NonNull SystemServerAdapter mockSystemServer, + @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; mBtHelper = new BtHelper(this); mDeviceInventory = mockDeviceInventory; mSystemServer = mockSystemServer; + mAudioSystem = audioSystem; init(); } @@ -450,7 +455,7 @@ import java.util.concurrent.atomic.AtomicBoolean; AudioAttributes attr = AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType( AudioSystem.STREAM_VOICE_CALL); - List<AudioDeviceAttributes> devices = AudioSystem.getDevicesForAttributes( + List<AudioDeviceAttributes> devices = mAudioSystem.getDevicesForAttributes( attr, false /* forVolume */); if (devices.isEmpty()) { if (mAudioService.isPlatformVoice()) { @@ -1225,7 +1230,7 @@ import java.util.concurrent.atomic.AtomicBoolean; Log.v(TAG, "onSetForceUse(useCase<" + useCase + ">, config<" + config + ">, fromA2dp<" + fromA2dp + ">, eventSource<" + eventSource + ">)"); } - AudioSystem.setForceUse(useCase, config); + mAudioSystem.setForceUse(useCase, config); } private void onSendBecomingNoisyIntent() { @@ -1863,9 +1868,9 @@ import java.util.concurrent.atomic.AtomicBoolean; if (preferredCommunicationDevice == null || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { - AudioSystem.setParameters("BT_SCO=off"); + mAudioSystem.setParameters("BT_SCO=off"); } else { - AudioSystem.setParameters("BT_SCO=on"); + mAudioSystem.setParameters("BT_SCO=on"); } if (preferredCommunicationDevice == null) { AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice(); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 5275b7c9868b..14784303e8b5 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -186,6 +186,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent; import com.android.server.audio.AudioServiceEvents.PhoneStateEvent; +import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent; import com.android.server.audio.AudioServiceEvents.VolumeEvent; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; @@ -1208,7 +1209,7 @@ public class AudioService extends IAudioService.Stub mUseFixedVolume = mContext.getResources().getBoolean( com.android.internal.R.bool.config_useFixedVolume); - mDeviceBroker = new AudioDeviceBroker(mContext, this); + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mRecordMonitor = new RecordingActivityMonitor(mContext); mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true); @@ -1640,7 +1641,7 @@ public class AudioService extends IAudioService.Stub synchronized (mSettingsLock) { final int forDock = mDockAudioMediaEnabled ? - AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE; + AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE; mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, forDock, "onAudioServerDied"); sendEncodedSurroundMode(mContentResolver, "onAudioServerDied"); sendEnabledSurroundFormats(mContentResolver, true); @@ -2261,9 +2262,10 @@ public class AudioService extends IAudioService.Stub SENDMSG_QUEUE, AudioSystem.FOR_DOCK, mDockAudioMediaEnabled ? - AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE, + AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE, new String("readDockAudioSettings"), 0); + } @@ -3604,9 +3606,11 @@ public class AudioService extends IAudioService.Stub setRingerMode(getNewRingerMode(stream, index, flags), TAG + ".onSetStreamVolume", false /*external*/); } - // setting non-zero volume for a muted stream unmutes the stream and vice versa, + // setting non-zero volume for a muted stream unmutes the stream and vice versa + // (only when changing volume for the current device), // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements - if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) { + if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) + && (getDeviceForStream(stream) == device)) { mStreamStates[stream].mute(index == 0); } } @@ -3744,19 +3748,30 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(ada); Objects.requireNonNull(callingPackage); - AudioService.sVolumeLogger.loglogi("setDeviceVolume" + " from:" + callingPackage + " " - + vi + " " + ada, TAG); - if (!vi.hasStreamType()) { Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception()); return; } + int index = vi.getVolumeIndex(); if (index == VolumeInfo.INDEX_NOT_SET && !vi.hasMuteCommand()) { throw new IllegalArgumentException( "changing device volume requires a volume index or mute command"); } + // force a cache clear to force reevaluating stream type to audio device selection + // that can interfere with the sending of the VOLUME_CHANGED_ACTION intent + // TODO change cache management to not rely only on invalidation, but on "do not trust" + // moments when routing is in flux. + mAudioSystem.clearRoutingCache(); + + // log the current device that will be used when evaluating the sending of the + // VOLUME_CHANGED_ACTION intent to see if the current device is the one being modified + final int currDev = getDeviceForStream(vi.getStreamType()); + + AudioService.sVolumeLogger.log(new DeviceVolumeEvent(vi.getStreamType(), index, ada, + currDev, callingPackage)); + // TODO handle unmuting of current audio device // if a stream is not muted but the VolumeInfo is for muting, set the volume index // for the device to min volume @@ -3840,11 +3855,11 @@ public class AudioService extends IAudioService.Stub return; } - final AudioEventLogger.Event event = (device == null) - ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType, - index/*val1*/, flags/*val2*/, callingPackage) - : new DeviceVolumeEvent(streamType, index, device, callingPackage); - sVolumeLogger.log(event); + if (device == null) { + // call was already logged in setDeviceVolume() + sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType, + index/*val1*/, flags/*val2*/, callingPackage)); + } setStreamVolume(streamType, index, flags, device, callingPackage, callingPackage, attributionTag, Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission()); @@ -4245,7 +4260,11 @@ public class AudioService extends IAudioService.Stub maybeSendSystemAudioStatusCommand(false); } } - sendVolumeUpdate(streamType, oldIndex, index, flags, device); + if (ada == null) { + // only non-null when coming here from setDeviceVolume + // TODO change test to check early if device is current device or not + sendVolumeUpdate(streamType, oldIndex, index, flags, device); + } } @@ -7989,6 +8008,8 @@ public class AudioService extends IAudioService.Stub mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex); mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, mStreamVolumeAlias[mStreamType]); + AudioService.sVolumeLogger.log(new VolChangedBroadcastEvent( + mStreamType, mStreamVolumeAlias[mStreamType], index)); sendBroadcastToAll(mVolumeChanged); } } @@ -10162,7 +10183,7 @@ public class AudioService extends IAudioService.Stub static final int LOG_NB_EVENTS_PHONE_STATE = 20; static final int LOG_NB_EVENTS_DEVICE_CONNECTION = 50; static final int LOG_NB_EVENTS_FORCE_USE = 20; - static final int LOG_NB_EVENTS_VOLUME = 40; + static final int LOG_NB_EVENTS_VOLUME = 100; static final int LOG_NB_EVENTS_DYN_POLICY = 10; static final int LOG_NB_EVENTS_SPATIAL = 30; diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index 30a9e0a70e96..c2c3f028abdb 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -147,19 +147,42 @@ public class AudioServiceEvents { } } + static final class VolChangedBroadcastEvent extends AudioEventLogger.Event { + final int mStreamType; + final int mAliasStreamType; + final int mIndex; + + VolChangedBroadcastEvent(int stream, int alias, int index) { + mStreamType = stream; + mAliasStreamType = alias; + mIndex = index; + } + + @Override + public String eventToString() { + return new StringBuilder("sending VOLUME_CHANGED stream:") + .append(AudioSystem.streamToString(mStreamType)) + .append(" index:").append(mIndex) + .append(" alias:").append(AudioSystem.streamToString(mAliasStreamType)) + .toString(); + } + } + static final class DeviceVolumeEvent extends AudioEventLogger.Event { final int mStream; final int mVolIndex; final String mDeviceNativeType; final String mDeviceAddress; final String mCaller; + final int mDeviceForStream; DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device, - String callingPackage) { + int deviceForStream, String callingPackage) { mStream = streamType; mVolIndex = index; mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType()); mDeviceAddress = device.getAddress(); + mDeviceForStream = deviceForStream; mCaller = callingPackage; // log metrics new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT) @@ -180,7 +203,9 @@ public class AudioServiceEvents { .append(" index:").append(mVolIndex) .append(" device:").append(mDeviceNativeType) .append(" addr:").append(mDeviceAddress) - .append(") from ").append(mCaller).toString(); + .append(") from ").append(mCaller) + .append(" currDevForStream:Ox").append(Integer.toHexString(mDeviceForStream)) + .toString(); } } diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index c3754eb3e44c..258837116cd6 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -105,6 +105,13 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } } + public void clearRoutingCache() { + if (DEBUG_CACHE) { + Log.d(TAG, "---- routing cache clear (from java) ----------"); + } + invalidateRoutingCache(); + } + /** * Implementation of AudioSystem.VolumeRangeInitRequestCallback */ @@ -337,6 +344,7 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, * @return */ public int setParameters(String keyValuePairs) { + invalidateRoutingCache(); return AudioSystem.setParameters(keyValuePairs); } diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 54be4bbd8f82..186294228d11 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -58,13 +58,15 @@ import java.util.function.Consumer; public final class PlaybackActivityMonitor implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer { - public static final String TAG = "AudioService.PlaybackActivityMonitor"; + public static final String TAG = "AS.PlayActivityMonitor"; /*package*/ static final boolean DEBUG = false; /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1; /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2; /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3; + /*package*/ static final int VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID = 4; + // ducking settings for a "normal duck" at -14dB private static final VolumeShaper.Configuration DUCK_VSHAPE = new VolumeShaper.Configuration.Builder() .setId(VOLUME_SHAPER_SYSTEM_DUCK_ID) @@ -78,6 +80,22 @@ public final class PlaybackActivityMonitor .build(); private static final VolumeShaper.Configuration DUCK_ID = new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_DUCK_ID); + + // ducking settings for a "strong duck" at -35dB (attenuation factor of 0.017783) + private static final VolumeShaper.Configuration STRONG_DUCK_VSHAPE = + new VolumeShaper.Configuration.Builder() + .setId(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID) + .setCurve(new float[] { 0.f, 1.f } /* times */, + new float[] { 1.f, 0.017783f } /* volumes */) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(MediaFocusControl.getFocusRampTimeMs( + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build())) + .build(); + private static final VolumeShaper.Configuration STRONG_DUCK_ID = + new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID); + private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) .createIfNeeded() @@ -659,11 +677,23 @@ public final class PlaybackActivityMonitor // add the players eligible for ducking to the list, and duck them // (if apcsToDuck is empty, this will at least mark this uid as ducked, so when // players of the same uid start, they will be ducked by DuckingManager.checkDuck()) - mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck); + mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck, reqCausesStrongDuck(winner)); } return true; } + private boolean reqCausesStrongDuck(FocusRequester requester) { + if (requester.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) { + return false; + } + final int reqUsage = requester.getAudioAttributes().getUsage(); + if ((reqUsage == AudioAttributes.USAGE_ASSISTANT) + || (reqUsage == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) { + return true; + } + return false; + } + @Override public void restoreVShapedPlayers(@NonNull FocusRequester winner) { if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); } @@ -939,10 +969,11 @@ public final class PlaybackActivityMonitor private static final class DuckingManager { private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>(); - synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) { + synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck, + boolean requestCausesStrongDuck) { if (DEBUG) { Log.v(TAG, "DuckingManager: duckUid() uid:"+ uid); } if (!mDuckers.containsKey(uid)) { - mDuckers.put(uid, new DuckedApp(uid)); + mDuckers.put(uid, new DuckedApp(uid, requestCausesStrongDuck)); } final DuckedApp da = mDuckers.get(uid); for (AudioPlaybackConfiguration apc : apcsToDuck) { @@ -989,10 +1020,13 @@ public final class PlaybackActivityMonitor private static final class DuckedApp { private final int mUid; + /** determines whether ducking is done with DUCK_VSHAPE or STRONG_DUCK_VSHAPE */ + private final boolean mUseStrongDuck; private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>(); - DuckedApp(int uid) { + DuckedApp(int uid, boolean useStrongDuck) { mUid = uid; + mUseStrongDuck = useStrongDuck; } void dump(PrintWriter pw) { @@ -1013,9 +1047,9 @@ public final class PlaybackActivityMonitor return; } try { - sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG)); + sEventLogger.log((new DuckEvent(apc, skipRamp, mUseStrongDuck)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( - DUCK_VSHAPE, + mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE, skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); mDuckedPlayers.add(piid); } catch (Exception e) { @@ -1031,7 +1065,7 @@ public final class PlaybackActivityMonitor sEventLogger.log((new AudioEventLogger.StringEvent("unducking piid:" + piid)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( - DUCK_ID, + mUseStrongDuck ? STRONG_DUCK_ID : DUCK_ID, VolumeShaper.Operation.REVERSE); } catch (Exception e) { Log.e(TAG, "Error unducking player piid:" + piid + " uid:" + mUid, e); @@ -1146,13 +1180,17 @@ public final class PlaybackActivityMonitor } static final class DuckEvent extends VolumeShaperEvent { + final boolean mUseStrongDuck; + @Override String getVSAction() { - return "ducking"; + return mUseStrongDuck ? "ducking (strong)" : "ducking"; } - DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) { + DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck) + { super(apc, skipRamp); + mUseStrongDuck = useStrongDuck; } } diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index 57ea812dbb3a..1924f3c92956 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -96,7 +96,11 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { Slog.d(TAG, "Remove onClientFinished: " + clientMonitor + ", success: " + success); - mCallback.onClientFinished(InternalCleanupClient.this, success); + if (mUnknownHALTemplates.isEmpty()) { + mCallback.onClientFinished(InternalCleanupClient.this, success); + } else { + startCleanupUnknownHalTemplates(); + } } }; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 05e83da6a107..787bfb00a554 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,15 +16,11 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; -import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; -import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; - import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskStackListener; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; -import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.common.ICancellationSignal; @@ -92,7 +88,6 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> private long mSideFpsLastAcquireStartTime; private Runnable mAuthSuccessRunnable; private final Clock mClock; - private boolean mDidFinishSfps; FingerprintAuthenticationClient( @NonNull Context context, @@ -198,9 +193,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> @Override protected void handleLifecycleAfterAuth(boolean authenticated) { - if (authenticated && !mDidFinishSfps) { + if (authenticated) { mCallback.onClientFinished(this, true /* success */); - mDidFinishSfps = true; } } @@ -210,13 +204,11 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> return false; } - public void handleAuthenticate( + @Override + public void onAuthenticated( BiometricAuthenticator.Identifier identifier, boolean authenticated, ArrayList<Byte> token) { - if (authenticated && mSensorProps.isAnySidefpsType()) { - Slog.i(TAG, "(sideFPS): No power press detected, sending auth"); - } super.onAuthenticated(identifier, authenticated, token); if (authenticated) { mState = STATE_STOPPED; @@ -227,72 +219,11 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> } @Override - public void onAuthenticated( - BiometricAuthenticator.Identifier identifier, - boolean authenticated, - ArrayList<Byte> token) { - - mHandler.post( - () -> { - long delay = 0; - if (authenticated && mSensorProps.isAnySidefpsType()) { - delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; - - if (mSideFpsLastAcquireStartTime != -1) { - delay = Math.max(0, - delay - (mClock.millis() - mSideFpsLastAcquireStartTime)); - } - - Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps " - + "waiting for power until: " + delay + "ms"); - } - - if (mHandler.hasMessages(MESSAGE_FINGER_UP)) { - Slog.i(TAG, "Finger up detected, sending auth"); - delay = 0; - } - - mAuthSuccessRunnable = - () -> handleAuthenticate(identifier, authenticated, token); - mHandler.postDelayed( - mAuthSuccessRunnable, - MESSAGE_AUTH_SUCCESS, - delay); - }); - } - - @Override public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) { // For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off // for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); - if (mSensorProps.isAnySidefpsType()) { - if (acquiredInfo == FINGERPRINT_ACQUIRED_START) { - mSideFpsLastAcquireStartTime = mClock.millis(); - } - final boolean shouldLookForVendor = - mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR; - final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage; - final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage; - final boolean ignorePowerPress = - acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch); - - if (ignorePowerPress) { - Slog.d(TAG, "(sideFPS) onFingerUp"); - mHandler.post(() -> { - if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) { - Slog.d(TAG, "(sideFPS) skipping wait for power"); - mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); - mHandler.post(mAuthSuccessRunnable); - } else { - mHandler.postDelayed(() -> { - }, MESSAGE_FINGER_UP, mFingerUpIgnoresPower); - } - }); - } - } - } @Override @@ -488,22 +419,5 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> } @Override - public void onPowerPressed() { - if (mSensorProps.isAnySidefpsType()) { - Slog.i(TAG, "(sideFPS): onPowerPressed"); - mHandler.post(() -> { - if (mDidFinishSfps) { - return; - } - Slog.i(TAG, "(sideFPS): finishing auth"); - // Ignore auths after a power has been detected - mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); - // Do not call onError() as that will send an additional callback to coex. - mDidFinishSfps = true; - onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true); - stopHalOperation(); - mSensorOverlays.hide(getSensorId()); - }); - } - } + public void onPowerPressed() { } } diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java index 9dd2f8408c56..b9ca57e70b3e 100644 --- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java +++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java @@ -76,6 +76,10 @@ class DeviceStateToLayoutMap { return layout; } + int size() { + return mLayoutMap.size(); + } + private Layout createLayout(int state) { if (mLayoutMap.contains(state)) { Slog.e(TAG, "Attempted to create a second layout for state " + state); diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index fa812c163643..3d81044ffda6 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -402,6 +402,8 @@ public class DisplayDeviceConfig { private static final String STABLE_ID_SUFFIX_FORMAT = "id_%d"; private static final String NO_SUFFIX_FORMAT = "%d"; private static final long STABLE_FLAG = 1L << 62; + private static final int DEFAULT_PEAK_REFRESH_RATE = 0; + private static final int DEFAULT_REFRESH_RATE = 60; private static final int DEFAULT_LOW_REFRESH_RATE = 60; private static final int DEFAULT_HIGH_REFRESH_RATE = 0; private static final int[] DEFAULT_BRIGHTNESS_THRESHOLDS = new int[]{}; @@ -570,17 +572,29 @@ public class DisplayDeviceConfig { * using higher refresh rates, even if display modes with higher refresh rates are available * from hardware composer. Only has an effect if the value is non-zero. */ - private int mDefaultHighRefreshRate = DEFAULT_HIGH_REFRESH_RATE; + private int mDefaultPeakRefreshRate = DEFAULT_PEAK_REFRESH_RATE; /** * The default refresh rate for a given device. This value sets the higher default * refresh rate. If the hardware composer on the device supports display modes with * a higher refresh rate than the default value specified here, the framework may use those * higher refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling - * setFrameRate(). We have historically allowed fallback to mDefaultHighRefreshRate if - * mDefaultLowRefreshRate is set to 0, but this is not supported anymore. + * setFrameRate(). We have historically allowed fallback to mDefaultPeakRefreshRate if + * mDefaultRefreshRate is set to 0, but this is not supported anymore. */ - private int mDefaultLowRefreshRate = DEFAULT_LOW_REFRESH_RATE; + private int mDefaultRefreshRate = DEFAULT_REFRESH_RATE; + + /** + * Default refresh rate in the high zone defined by brightness and ambient thresholds. + * If non-positive, then the refresh rate is unchanged even if thresholds are configured. + */ + private int mDefaultHighBlockingZoneRefreshRate = DEFAULT_HIGH_REFRESH_RATE; + + /** + * Default refresh rate in the zone defined by brightness and ambient thresholds. + * If non-positive, then the refresh rate is unchanged even if thresholds are configured. + */ + private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE; /** * The display uses different gamma curves for different refresh rates. It's hard for panel @@ -1295,15 +1309,29 @@ public class DisplayDeviceConfig { /** * @return Default peak refresh rate of the associated display */ - public int getDefaultHighRefreshRate() { - return mDefaultHighRefreshRate; + public int getDefaultPeakRefreshRate() { + return mDefaultPeakRefreshRate; } /** * @return Default refresh rate of the associated display */ - public int getDefaultLowRefreshRate() { - return mDefaultLowRefreshRate; + public int getDefaultRefreshRate() { + return mDefaultRefreshRate; + } + + /** + * @return Default refresh rate in the higher blocking zone of the associated display + */ + public int getDefaultHighBlockingZoneRefreshRate() { + return mDefaultHighBlockingZoneRefreshRate; + } + + /** + * @return Default refresh rate in the lower blocking zone of the associated display + */ + public int getDefaultLowBlockingZoneRefreshRate() { + return mDefaultLowBlockingZoneRefreshRate; } /** @@ -1441,8 +1469,10 @@ public class DisplayDeviceConfig { + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable + "\n" - + ", mDefaultRefreshRate= " + mDefaultLowRefreshRate - + ", mDefaultPeakRefreshRate= " + mDefaultHighRefreshRate + + ", mDefaultLowBlockingZoneRefreshRate= " + mDefaultLowBlockingZoneRefreshRate + + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate + + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate + + ", mDefaultRefreshRate= " + mDefaultRefreshRate + ", mLowDisplayBrightnessThresholds= " + Arrays.toString(mLowDisplayBrightnessThresholds) + ", mLowAmbientBrightnessThresholds= " @@ -1756,10 +1786,31 @@ public class DisplayDeviceConfig { BlockingZoneConfig higherBlockingZoneConfig = (refreshRateConfigs == null) ? null : refreshRateConfigs.getHigherBlockingZoneConfigs(); + loadPeakDefaultRefreshRate(refreshRateConfigs); + loadDefaultRefreshRate(refreshRateConfigs); loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig); loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig); } + private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) { + if (refreshRateConfigs == null || refreshRateConfigs.getDefaultPeakRefreshRate() == null) { + mDefaultPeakRefreshRate = mContext.getResources().getInteger( + R.integer.config_defaultPeakRefreshRate); + } else { + mDefaultPeakRefreshRate = + refreshRateConfigs.getDefaultPeakRefreshRate().intValue(); + } + } + + private void loadDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) { + if (refreshRateConfigs == null || refreshRateConfigs.getDefaultRefreshRate() == null) { + mDefaultRefreshRate = mContext.getResources().getInteger( + R.integer.config_defaultRefreshRate); + } else { + mDefaultRefreshRate = + refreshRateConfigs.getDefaultRefreshRate().intValue(); + } + } /** * Loads the refresh rate configurations pertaining to the upper blocking zones. @@ -1784,10 +1835,10 @@ public class DisplayDeviceConfig { private void loadHigherBlockingZoneDefaultRefreshRate( BlockingZoneConfig upperBlockingZoneConfig) { if (upperBlockingZoneConfig == null) { - mDefaultHighRefreshRate = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultPeakRefreshRate); + mDefaultHighBlockingZoneRefreshRate = mContext.getResources().getInteger( + com.android.internal.R.integer.config_fixedRefreshRateInHighZone); } else { - mDefaultHighRefreshRate = + mDefaultHighBlockingZoneRefreshRate = upperBlockingZoneConfig.getDefaultRefreshRate().intValue(); } } @@ -1799,10 +1850,10 @@ public class DisplayDeviceConfig { private void loadLowerBlockingZoneDefaultRefreshRate( BlockingZoneConfig lowerBlockingZoneConfig) { if (lowerBlockingZoneConfig == null) { - mDefaultLowRefreshRate = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultRefreshRate); + mDefaultLowBlockingZoneRefreshRate = mContext.getResources().getInteger( + com.android.internal.R.integer.config_defaultRefreshRateInZone); } else { - mDefaultLowRefreshRate = + mDefaultLowBlockingZoneRefreshRate = lowerBlockingZoneConfig.getDefaultRefreshRate().intValue(); } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 8f35924128bb..5cfe65baeb9d 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -104,6 +104,7 @@ import android.os.UserManager; import android.provider.Settings; import android.sysprop.DisplayProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.IntArray; @@ -256,6 +257,13 @@ public final class DisplayManagerService extends SystemService { final SparseArray<Pair<IVirtualDevice, DisplayWindowPolicyController>> mDisplayWindowPolicyControllers = new SparseArray<>(); + /** + * Map of every internal primary display device {@link HighBrightnessModeMetadata}s indexed by + * {@link DisplayDevice#mUniqueId}. + */ + public final ArrayMap<String, HighBrightnessModeMetadata> mHighBrightnessModeMetadataMap = + new ArrayMap<>(); + // List of all currently registered display adapters. private final ArrayList<DisplayAdapter> mDisplayAdapters = new ArrayList<DisplayAdapter>(); @@ -1570,7 +1578,16 @@ public final class DisplayManagerService extends SystemService { DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { - dpc.onDisplayChanged(); + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: " + + display.getDisplayIdLocked()); + return; + } + + final String uniqueId = device.getUniqueId(); + HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId); + dpc.onDisplayChanged(hbmMetadata); } } @@ -1627,7 +1644,15 @@ public final class DisplayManagerService extends SystemService { final int displayId = display.getDisplayIdLocked(); final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { - dpc.onDisplayChanged(); + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: " + + display.getDisplayIdLocked()); + return; + } + final String uniqueId = device.getUniqueId(); + HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId); + dpc.onDisplayChanged(hbmMetadata); } } @@ -2611,6 +2636,31 @@ public final class DisplayManagerService extends SystemService { mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked); } + private HighBrightnessModeMetadata getHighBrightnessModeMetadata(LogicalDisplay display) { + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: " + + display.getDisplayIdLocked()); + return null; + } + + // HBM brightness mode is only applicable to internal physical displays. + if (display.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) { + return null; + } + + final String uniqueId = device.getUniqueId(); + + if (mHighBrightnessModeMetadataMap.containsKey(uniqueId)) { + return mHighBrightnessModeMetadataMap.get(uniqueId); + } + + // HBM Time info not present. Create a new one for this physical display. + HighBrightnessModeMetadata hbmInfo = new HighBrightnessModeMetadata(); + mHighBrightnessModeMetadataMap.put(uniqueId, hbmInfo); + return hbmInfo; + } + private void addDisplayPowerControllerLocked(LogicalDisplay display) { if (mPowerHandler == null) { // initPowerManagement has not yet been called. @@ -2622,10 +2672,18 @@ public final class DisplayManagerService extends SystemService { final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore, display, mSyncRoot); + + // If display is internal and has a HighBrightnessModeMetadata mapping, use that. + // Or create a new one and use that. + // We also need to pass a mapping of the HighBrightnessModeTimeInfoMap to + // displayPowerController, so the hbm info can be correctly associated + // with the corresponding displaydevice. + HighBrightnessModeMetadata hbmMetadata = getHighBrightnessModeMetadata(display); + final DisplayPowerController displayPowerController = new DisplayPowerController( mContext, mDisplayPowerCallbacks, mPowerHandler, mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting, - () -> handleBrightnessChange(display)); + () -> handleBrightnessChange(display), hbmMetadata); mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController); } diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java index aafba5a2a1b4..fdfc20afee79 100644 --- a/services/core/java/com/android/server/display/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/DisplayModeDirector.java @@ -1169,7 +1169,7 @@ public class DisplayModeDirector { mDefaultRefreshRate = (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( R.integer.config_defaultRefreshRate) - : (float) displayDeviceConfig.getDefaultLowRefreshRate(); + : (float) displayDeviceConfig.getDefaultRefreshRate(); } public void observe() { @@ -1256,7 +1256,7 @@ public class DisplayModeDirector { defaultPeakRefreshRate = (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( R.integer.config_defaultPeakRefreshRate) - : (float) displayDeviceConfig.getDefaultHighRefreshRate(); + : (float) displayDeviceConfig.getDefaultPeakRefreshRate(); } mDefaultPeakRefreshRate = defaultPeakRefreshRate; } @@ -1612,8 +1612,26 @@ public class DisplayModeDirector { return mHighAmbientBrightnessThresholds; } + /** + * @return the refresh rate to lock to when in a high brightness zone + */ + @VisibleForTesting + int getRefreshRateInHighZone() { + return mRefreshRateInHighZone; + } + + /** + * @return the refresh rate to lock to when in a low brightness zone + */ + @VisibleForTesting + int getRefreshRateInLowZone() { + return mRefreshRateInLowZone; + } + private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig, boolean attemptLoadingFromDeviceConfig) { + loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig); + loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig); mLowDisplayBrightnessThresholds = loadBrightnessThresholds( () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(), () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(), @@ -1634,6 +1652,44 @@ public class DisplayModeDirector { } } + private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig, + boolean attemptLoadingFromDeviceConfig) { + int refreshRateInLowZone = + (displayDeviceConfig == null) ? mContext.getResources().getInteger( + R.integer.config_defaultRefreshRateInZone) + : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(); + if (attemptLoadingFromDeviceConfig) { + try { + refreshRateInLowZone = mDeviceConfig.getInt( + DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, + refreshRateInLowZone); + } catch (Exception exception) { + // Do nothing + } + } + mRefreshRateInLowZone = refreshRateInLowZone; + } + + private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig, + boolean attemptLoadingFromDeviceConfig) { + int refreshRateInHighZone = + (displayDeviceConfig == null) ? mContext.getResources().getInteger( + R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig + .getDefaultHighBlockingZoneRefreshRate(); + if (attemptLoadingFromDeviceConfig) { + try { + refreshRateInHighZone = mDeviceConfig.getInt( + DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE, + refreshRateInHighZone); + } catch (Exception exception) { + // Do nothing + } + } + mRefreshRateInHighZone = refreshRateInHighZone; + } + private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig, boolean attemptLoadingFromDeviceConfig) { mHighDisplayBrightnessThresholds = loadBrightnessThresholds( @@ -1687,14 +1743,6 @@ public class DisplayModeDirector { } /** - * @return the refresh to lock to when in a low brightness zone - */ - @VisibleForTesting - int getRefreshRateInLowZone() { - return mRefreshRateInLowZone; - } - - /** * @return the display brightness thresholds for the low brightness zones */ @VisibleForTesting @@ -1739,8 +1787,17 @@ public class DisplayModeDirector { mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds; } - mRefreshRateInLowZone = mDeviceConfigDisplaySettings.getRefreshRateInLowZone(); - mRefreshRateInHighZone = mDeviceConfigDisplaySettings.getRefreshRateInHighZone(); + final int refreshRateInLowZone = mDeviceConfigDisplaySettings + .getRefreshRateInLowZone(); + if (refreshRateInLowZone != -1) { + mRefreshRateInLowZone = refreshRateInLowZone; + } + + final int refreshRateInHighZone = mDeviceConfigDisplaySettings + .getRefreshRateInHighZone(); + if (refreshRateInHighZone != -1) { + mRefreshRateInHighZone = refreshRateInHighZone; + } restartObserver(); mDeviceConfigDisplaySettings.startListening(); @@ -1794,6 +1851,10 @@ public class DisplayModeDirector { restartObserver(); } + /** + * Used to reload the lower blocking zone refresh rate in case of changes in the + * DeviceConfig properties. + */ public void onDeviceConfigRefreshRateInLowZoneChanged(int refreshRate) { if (refreshRate != mRefreshRateInLowZone) { mRefreshRateInLowZone = refreshRate; @@ -1817,6 +1878,10 @@ public class DisplayModeDirector { restartObserver(); } + /** + * Used to reload the higher blocking zone refresh rate in case of changes in the + * DeviceConfig properties. + */ public void onDeviceConfigRefreshRateInHighZoneChanged(int refreshRate) { if (refreshRate != mRefreshRateInHighZone) { mRefreshRateInHighZone = refreshRate; @@ -2664,15 +2729,10 @@ public class DisplayModeDirector { } public int getRefreshRateInLowZone() { - int defaultRefreshRateInZone = mContext.getResources().getInteger( - R.integer.config_defaultRefreshRateInZone); - - int refreshRate = mDeviceConfig.getInt( + return mDeviceConfig.getInt( DeviceConfig.NAMESPACE_DISPLAY_MANAGER, - DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, - defaultRefreshRateInZone); + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1); - return refreshRate; } /* @@ -2694,15 +2754,10 @@ public class DisplayModeDirector { } public int getRefreshRateInHighZone() { - int defaultRefreshRateInZone = mContext.getResources().getInteger( - R.integer.config_fixedRefreshRateInHighZone); - - int refreshRate = mDeviceConfig.getInt( + return mDeviceConfig.getInt( DeviceConfig.NAMESPACE_DISPLAY_MANAGER, DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE, - defaultRefreshRateInZone); - - return refreshRate; + -1); } public int getRefreshRateInHbmSunlight() { @@ -2750,23 +2805,29 @@ public class DisplayModeDirector { int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds(); int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds(); - int refreshRateInLowZone = getRefreshRateInLowZone(); + final int refreshRateInLowZone = getRefreshRateInLowZone(); mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED, new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds)) .sendToTarget(); - mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone, 0) + + if (refreshRateInLowZone != -1) { + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone) .sendToTarget(); + } int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds(); int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds(); - int refreshRateInHighZone = getRefreshRateInHighZone(); + final int refreshRateInHighZone = getRefreshRateInHighZone(); mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED, new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds)) .sendToTarget(); - mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0) + + if (refreshRateInHighZone != -1) { + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone) .sendToTarget(); + } final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight(); mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED, diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index b73731752149..eeb261cdf951 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -391,6 +391,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private float[] mNitsRange; private final HighBrightnessModeController mHbmController; + private final HighBrightnessModeMetadata mHighBrightnessModeMetadata; private final BrightnessThrottler mBrightnessThrottler; @@ -511,7 +512,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call DisplayPowerCallbacks callbacks, Handler handler, SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay, BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting, - Runnable onBrightnessChangeRunnable) { + Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata) { mLogicalDisplay = logicalDisplay; mDisplayId = mLogicalDisplay.getDisplayIdLocked(); final String displayIdStr = "[" + mDisplayId + "]"; @@ -521,6 +522,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mSuspendBlockerIdProxPositive = displayIdStr + "prox positive"; mSuspendBlockerIdProxNegative = displayIdStr + "prox negative"; mSuspendBlockerIdProxDebounce = displayIdStr + "prox debounce"; + mHighBrightnessModeMetadata = hbmMetadata; mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); @@ -793,7 +795,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * of each display need to be properly reflected in AutomaticBrightnessController. */ @GuardedBy("DisplayManagerService.mSyncRoot") - public void onDisplayChanged() { + public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) { final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); if (device == null) { Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: " @@ -815,11 +817,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mUniqueDisplayId = uniqueId; mDisplayStatsId = mUniqueDisplayId.hashCode(); mDisplayDeviceConfig = config; - loadFromDisplayDeviceConfig(token, info); + loadFromDisplayDeviceConfig(token, info, hbmMetadata); - // Since the underlying display-device changed, we really don't know the - // last command that was sent to change it's state. Lets assume it is off and we - // trigger a change immediately. + /// Since the underlying display-device changed, we really don't know the + // last command that was sent to change it's state. Lets assume it is unknown so + // that we trigger a change immediately. mPowerState.resetScreenState(); } if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) { @@ -872,7 +874,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info) { + private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info, + HighBrightnessModeMetadata hbmMetadata) { // All properties that depend on the associated DisplayDevice and the DDC must be // updated here. loadBrightnessRampRates(); @@ -885,6 +888,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRampIncreaseMaxTimeMillis, mBrightnessRampDecreaseMaxTimeMillis); } + mHbmController.setHighBrightnessModeMetadata(hbmMetadata); mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, mDisplayDeviceConfig.getHighBrightnessModeData(), new HighBrightnessModeController.HdrBrightnessDeviceConfig() { @@ -1965,7 +1969,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.update(); } - }, mContext); + }, mHighBrightnessModeMetadata, mContext); } private BrightnessThrottler createBrightnessThrottlerLocked() { diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index 7d1396d7e413..2c257a17af91 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -340,20 +340,12 @@ final class DisplayPowerState { } /** - * Resets the screen state to {@link Display#STATE_OFF}. Even though we do not know the last - * state that was sent to the underlying display-device, we assume it is off. - * - * We do not set the screen state to {@link Display#STATE_UNKNOWN} to avoid getting in the state - * where PhotonicModulator holds onto the lock. This happens because we currently try to keep - * the mScreenState and mPendingState in sync, however if the screenState is set to - * {@link Display#STATE_UNKNOWN} here, mPendingState will get progressed to this, which will - * force the PhotonicModulator thread to wait onto the lock to take it out of that state. - * b/262294651 for more info. + * Resets the screen state to unknown. Useful when the underlying display-device changes for the + * LogicalDisplay and we do not know the last state that was sent to it. */ void resetScreenState() { - mScreenState = Display.STATE_OFF; + mScreenState = Display.STATE_UNKNOWN; mScreenReady = false; - scheduleScreenUpdate(); } private void scheduleScreenUpdate() { @@ -514,6 +506,8 @@ final class DisplayPowerState { boolean valid = state != Display.STATE_UNKNOWN && !Float.isNaN(brightnessState); boolean changed = stateChanged || backlightChanged; if (!valid || !changed) { + mStateChangeInProgress = false; + mBacklightChangeInProgress = false; try { mLock.wait(); } catch (InterruptedException ex) { diff --git a/services/core/java/com/android/server/display/HbmEvent.java b/services/core/java/com/android/server/display/HbmEvent.java new file mode 100644 index 000000000000..5675e2f69230 --- /dev/null +++ b/services/core/java/com/android/server/display/HbmEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + + +/** + * Represents an event in which High Brightness Mode was enabled. + */ +class HbmEvent { + private long mStartTimeMillis; + private long mEndTimeMillis; + + HbmEvent(long startTimeMillis, long endTimeMillis) { + this.mStartTimeMillis = startTimeMillis; + this.mEndTimeMillis = endTimeMillis; + } + + public long getStartTimeMillis() { + return mStartTimeMillis; + } + + public long getEndTimeMillis() { + return mEndTimeMillis; + } + + @Override + public String toString() { + return "HbmEvent: {startTimeMillis:" + mStartTimeMillis + ", endTimeMillis: " + + mEndTimeMillis + "}, total: " + + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]"; + } +} diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index 0b9d4debd16f..ac32d53daeab 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -42,8 +42,8 @@ import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData; import com.android.server.display.DisplayManagerService.Clock; import java.io.PrintWriter; +import java.util.ArrayDeque; import java.util.Iterator; -import java.util.LinkedList; /** * Controls the status of high-brightness mode for devices that support it. This class assumes that @@ -105,30 +105,24 @@ class HighBrightnessModeController { private int mHbmStatsState = FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF; /** - * If HBM is currently running, this is the start time for the current HBM session. + * If HBM is currently running, this is the start time and set of all events, + * for the current HBM session. */ - private long mRunningStartTimeMillis = -1; - - /** - * List of previous HBM-events ordered from most recent to least recent. - * Meant to store only the events that fall into the most recent - * {@link mHbmData.timeWindowMillis}. - */ - private LinkedList<HbmEvent> mEvents = new LinkedList<>(); + private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null; HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, - Runnable hbmChangeCallback, Context context) { + Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { this(new Injector(), handler, width, height, displayToken, displayUniqueId, brightnessMin, - brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, context); + brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, hbmMetadata, context); } @VisibleForTesting HighBrightnessModeController(Injector injector, Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, - Runnable hbmChangeCallback, Context context) { + Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { mInjector = injector; mContext = context; mClock = injector.getClock(); @@ -137,6 +131,7 @@ class HighBrightnessModeController { mBrightnessMin = brightnessMin; mBrightnessMax = brightnessMax; mHbmChangeCallback = hbmChangeCallback; + mHighBrightnessModeMetadata = hbmMetadata; mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler); mSettingsObserver = new SettingsObserver(mHandler); mRecalcRunnable = this::recalculateTimeAllowance; @@ -222,19 +217,22 @@ class HighBrightnessModeController { // If we are starting or ending a high brightness mode session, store the current // session in mRunningStartTimeMillis, or the old one in mEvents. - final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1; + final long runningStartTime = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + final boolean wasHbmDrainingAvailableTime = runningStartTime != -1; final boolean shouldHbmDrainAvailableTime = mBrightness > mHbmData.transitionPoint && !mIsHdrLayerPresent; if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) { final long currentTime = mClock.uptimeMillis(); if (shouldHbmDrainAvailableTime) { - mRunningStartTimeMillis = currentTime; + mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime); } else { - mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime)); - mRunningStartTimeMillis = -1; + final HbmEvent hbmEvent = new HbmEvent(runningStartTime, currentTime); + mHighBrightnessModeMetadata.addHbmEvent(hbmEvent); + mHighBrightnessModeMetadata.setRunningStartTimeMillis(-1); if (DEBUG) { - Slog.d(TAG, "New HBM event: " + mEvents.getFirst()); + Slog.d(TAG, "New HBM event: " + + mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst()); } } } @@ -260,6 +258,10 @@ class HighBrightnessModeController { mSettingsObserver.stopObserving(); } + void setHighBrightnessModeMetadata(HighBrightnessModeMetadata hbmInfo) { + mHighBrightnessModeMetadata = hbmInfo; + } + void resetHbmData(int width, int height, IBinder displayToken, String displayUniqueId, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg) { mWidth = width; @@ -316,20 +318,22 @@ class HighBrightnessModeController { pw.println(" mBrightnessMax=" + mBrightnessMax); pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis())); pw.println(" mIsTimeAvailable= " + mIsTimeAvailable); - pw.println(" mRunningStartTimeMillis=" + TimeUtils.formatUptime(mRunningStartTimeMillis)); + pw.println(" mRunningStartTimeMillis=" + + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis())); pw.println(" mIsThermalStatusWithinLimit=" + mIsThermalStatusWithinLimit); pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode); pw.println(" width*height=" + mWidth + "*" + mHeight); pw.println(" mEvents="); final long currentTime = mClock.uptimeMillis(); long lastStartTime = currentTime; - if (mRunningStartTimeMillis != -1) { - lastStartTime = dumpHbmEvent(pw, new HbmEvent(mRunningStartTimeMillis, currentTime)); + long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + if (runningStartTimeMillis != -1) { + lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime)); } - for (HbmEvent event : mEvents) { - if (lastStartTime > event.endTimeMillis) { + for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) { + if (lastStartTime > event.getEndTimeMillis()) { pw.println(" event: [normal brightness]: " - + TimeUtils.formatDuration(lastStartTime - event.endTimeMillis)); + + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis())); } lastStartTime = dumpHbmEvent(pw, event); } @@ -338,12 +342,12 @@ class HighBrightnessModeController { } private long dumpHbmEvent(PrintWriter pw, HbmEvent event) { - final long duration = event.endTimeMillis - event.startTimeMillis; + final long duration = event.getEndTimeMillis() - event.getStartTimeMillis(); pw.println(" event: [" - + TimeUtils.formatUptime(event.startTimeMillis) + ", " - + TimeUtils.formatUptime(event.endTimeMillis) + "] (" + + TimeUtils.formatUptime(event.getStartTimeMillis()) + ", " + + TimeUtils.formatUptime(event.getEndTimeMillis()) + "] (" + TimeUtils.formatDuration(duration) + ")"); - return event.startTimeMillis; + return event.getStartTimeMillis(); } private boolean isCurrentlyAllowed() { @@ -372,13 +376,15 @@ class HighBrightnessModeController { // First, lets see how much time we've taken for any currently running // session of HBM. - if (mRunningStartTimeMillis > 0) { - if (mRunningStartTimeMillis > currentTime) { + long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + if (runningStartTimeMillis > 0) { + if (runningStartTimeMillis > currentTime) { Slog.e(TAG, "Start time set to the future. curr: " + currentTime - + ", start: " + mRunningStartTimeMillis); - mRunningStartTimeMillis = currentTime; + + ", start: " + runningStartTimeMillis); + mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime); + runningStartTimeMillis = currentTime; } - timeAlreadyUsed = currentTime - mRunningStartTimeMillis; + timeAlreadyUsed = currentTime - runningStartTimeMillis; } if (DEBUG) { @@ -387,18 +393,19 @@ class HighBrightnessModeController { // Next, lets iterate through the history of previous sessions and add those times. final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis; - Iterator<HbmEvent> it = mEvents.iterator(); + Iterator<HbmEvent> it = mHighBrightnessModeMetadata.getHbmEventQueue().iterator(); while (it.hasNext()) { final HbmEvent event = it.next(); // If this event ended before the current Timing window, discard forever and ever. - if (event.endTimeMillis < windowstartTimeMillis) { + if (event.getEndTimeMillis() < windowstartTimeMillis) { it.remove(); continue; } - final long startTimeMillis = Math.max(event.startTimeMillis, windowstartTimeMillis); - timeAlreadyUsed += event.endTimeMillis - startTimeMillis; + final long startTimeMillis = Math.max(event.getStartTimeMillis(), + windowstartTimeMillis); + timeAlreadyUsed += event.getEndTimeMillis() - startTimeMillis; } if (DEBUG) { @@ -425,17 +432,18 @@ class HighBrightnessModeController { // Calculate the time at which we want to recalculate mIsTimeAvailable in case a lux or // brightness change doesn't happen before then. long nextTimeout = -1; + final ArrayDeque<HbmEvent> hbmEvents = mHighBrightnessModeMetadata.getHbmEventQueue(); if (mBrightness > mHbmData.transitionPoint) { // if we're in high-lux now, timeout when we run out of allowed time. nextTimeout = currentTime + remainingTime; - } else if (!mIsTimeAvailable && mEvents.size() > 0) { + } else if (!mIsTimeAvailable && hbmEvents.size() > 0) { // If we are not allowed...timeout when the oldest event moved outside of the timing // window by at least minTime. Basically, we're calculating the soonest time we can // get {@code timeMinMillis} back to us. final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis; - final HbmEvent lastEvent = mEvents.getLast(); + final HbmEvent lastEvent = hbmEvents.peekLast(); final long startTimePlusMinMillis = - Math.max(windowstartTimeMillis, lastEvent.startTimeMillis) + Math.max(windowstartTimeMillis, lastEvent.getStartTimeMillis()) + mHbmData.timeMinMillis; final long timeWhenMinIsGainedBack = currentTime + (startTimePlusMinMillis - windowstartTimeMillis) - remainingTime; @@ -459,9 +467,10 @@ class HighBrightnessModeController { + ", mUnthrottledBrightness: " + mUnthrottledBrightness + ", mThrottlingReason: " + BrightnessInfo.briMaxReasonToString(mThrottlingReason) - + ", RunningStartTimeMillis: " + mRunningStartTimeMillis + + ", RunningStartTimeMillis: " + + mHighBrightnessModeMetadata.getRunningStartTimeMillis() + ", nextTimeout: " + (nextTimeout != -1 ? (nextTimeout - currentTime) : -1) - + ", events: " + mEvents); + + ", events: " + hbmEvents); } if (nextTimeout != -1) { @@ -588,25 +597,6 @@ class HighBrightnessModeController { } } - /** - * Represents an event in which High Brightness Mode was enabled. - */ - private static class HbmEvent { - public long startTimeMillis; - public long endTimeMillis; - - HbmEvent(long startTimeMillis, long endTimeMillis) { - this.startTimeMillis = startTimeMillis; - this.endTimeMillis = endTimeMillis; - } - - @Override - public String toString() { - return "[Event: {" + startTimeMillis + ", " + endTimeMillis + "}, total: " - + ((endTimeMillis - startTimeMillis) / 1000) + "]"; - } - } - @VisibleForTesting class HdrListener extends SurfaceControlHdrLayerInfoListener { @Override diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java new file mode 100644 index 000000000000..37234ff0bf19 --- /dev/null +++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import java.util.ArrayDeque; + + +/** + * Represents High Brightness Mode metadata associated + * with a specific internal physical display. + * Required for separately storing data like time information, + * and related events when display was in HBM mode per + * physical internal display. + */ +class HighBrightnessModeMetadata { + /** + * Queue of previous HBM-events ordered from most recent to least recent. + * Meant to store only the events that fall into the most recent + * {@link HighBrightnessModeData#timeWindowMillis mHbmData.timeWindowMillis}. + */ + private final ArrayDeque<HbmEvent> mEvents = new ArrayDeque<>(); + + /** + * If HBM is currently running, this is the start time for the current HBM session. + */ + private long mRunningStartTimeMillis = -1; + + public long getRunningStartTimeMillis() { + return mRunningStartTimeMillis; + } + + public void setRunningStartTimeMillis(long setTime) { + mRunningStartTimeMillis = setTime; + } + + public ArrayDeque<HbmEvent> getHbmEventQueue() { + return mEvents; + } + + public void addHbmEvent(HbmEvent hbmEvent) { + mEvents.addFirst(hbmEvent); + } +} + diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index bf576b8909f2..375e51c672b5 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -372,12 +372,23 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { void setDeviceStateLocked(int state, boolean isOverrideActive) { Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted); + mPendingDeviceState = state; + + if (!mBootCompleted) { + // The boot animation might still be in progress, we do not want to switch states now + // as the boot animation would end up with an incorrect size. + if (DEBUG) { + Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState + + " until boot is completed"); + } + return; + } + // As part of a state transition, we may need to turn off some displays temporarily so that // the transition is smooth. Plus, on some devices, only one internal displays can be // on at a time. We use LogicalDisplay.setIsInTransition to mark a display that needs to be // temporarily turned off. resetLayoutLocked(mDeviceState, state, /* isStateChangeStarting= */ true); - mPendingDeviceState = state; final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState, mInteractive, mBootCompleted); final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState, @@ -424,6 +435,9 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { void onBootCompleted() { synchronized (mSyncRoot) { mBootCompleted = true; + if (mPendingDeviceState != DeviceStateManager.INVALID_DEVICE_STATE) { + setDeviceStateLocked(mPendingDeviceState, /* isOverrideActive= */ false); + } } } @@ -926,6 +940,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final int layerStack = assignLayerStackLocked(displayId); final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device); display.updateLocked(mDisplayDeviceRepo); + + final DisplayInfo info = display.getDisplayInfoLocked(); + if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) { + // If this is an internal display and the device uses a display layout configuration, + // the display should be disabled as later we will receive a device state update, which + // will tell us which internal displays should be enabled and which should be disabled. + display.setEnabledLocked(false); + } + mLogicalDisplays.put(displayId, display); return display; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 195101dd3c5c..f2f312a82519 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -239,7 +239,6 @@ import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationRecordProto; import android.service.notification.NotificationServiceDumpProto; import android.service.notification.NotificationStats; -import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeProto; @@ -8568,95 +8567,6 @@ public class NotificationManagerService extends SystemService { } } - static class NotificationRecordExtractorData { - // Class that stores any field in a NotificationRecord that can change via an extractor. - // Used to cache previous data used in a sort. - int mPosition; - int mVisibility; - boolean mShowBadge; - boolean mAllowBubble; - boolean mIsBubble; - NotificationChannel mChannel; - String mGroupKey; - ArrayList<String> mOverridePeople; - ArrayList<SnoozeCriterion> mSnoozeCriteria; - Integer mUserSentiment; - Integer mSuppressVisually; - ArrayList<Notification.Action> mSystemSmartActions; - ArrayList<CharSequence> mSmartReplies; - int mImportance; - - // These fields may not trigger a reranking but diffs here may be logged. - float mRankingScore; - boolean mIsConversation; - - NotificationRecordExtractorData(int position, int visibility, boolean showBadge, - boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey, - ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, - Integer userSentiment, Integer suppressVisually, - ArrayList<Notification.Action> systemSmartActions, - ArrayList<CharSequence> smartReplies, int importance, float rankingScore, - boolean isConversation) { - mPosition = position; - mVisibility = visibility; - mShowBadge = showBadge; - mAllowBubble = allowBubble; - mIsBubble = isBubble; - mChannel = channel; - mGroupKey = groupKey; - mOverridePeople = overridePeople; - mSnoozeCriteria = snoozeCriteria; - mUserSentiment = userSentiment; - mSuppressVisually = suppressVisually; - mSystemSmartActions = systemSmartActions; - mSmartReplies = smartReplies; - mImportance = importance; - mRankingScore = rankingScore; - mIsConversation = isConversation; - } - - // Returns whether the provided NotificationRecord differs from the cached data in any way. - // Should be guarded by mNotificationLock; not annotated here as this class is static. - boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) { - return mPosition != newPosition - || mVisibility != r.getPackageVisibilityOverride() - || mShowBadge != r.canShowBadge() - || mAllowBubble != r.canBubble() - || mIsBubble != r.getNotification().isBubbleNotification() - || !Objects.equals(mChannel, r.getChannel()) - || !Objects.equals(mGroupKey, r.getGroupKey()) - || !Objects.equals(mOverridePeople, r.getPeopleOverride()) - || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) - || !Objects.equals(mUserSentiment, r.getUserSentiment()) - || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects()) - || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) - || !Objects.equals(mSmartReplies, r.getSmartReplies()) - || mImportance != r.getImportance(); - } - - // Returns whether the NotificationRecord has a change from this data for which we should - // log an update. This method specifically targets fields that may be changed via - // adjustments from the assistant. - // - // Fields here are the union of things in NotificationRecordLogger.shouldLogReported - // and NotificationRecord.applyAdjustments. - // - // Should be guarded by mNotificationLock; not annotated here as this class is static. - boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) { - return mPosition != newPosition - || !Objects.equals(mChannel, r.getChannel()) - || !Objects.equals(mGroupKey, r.getGroupKey()) - || !Objects.equals(mOverridePeople, r.getPeopleOverride()) - || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) - || !Objects.equals(mUserSentiment, r.getUserSentiment()) - || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) - || !Objects.equals(mSmartReplies, r.getSmartReplies()) - || mImportance != r.getImportance() - || !r.rankingScoreMatches(mRankingScore) - || mIsConversation != r.isConversation(); - } - } - void handleRankingSort() { if (mRankingHelper == null) return; synchronized (mNotificationLock) { @@ -8682,7 +8592,8 @@ public class NotificationManagerService extends SystemService { r.getSmartReplies(), r.getImportance(), r.getRankingScore(), - r.isConversation()); + r.isConversation(), + r.getProposedImportance()); extractorDataBefore.put(r.getKey(), extractorData); mRankingHelper.extractSignals(r); } @@ -9977,7 +9888,8 @@ public class NotificationManagerService extends SystemService { record.getRankingScore() == 0 ? RANKING_UNCHANGED : (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED), - record.getNotification().isBubbleNotification() + record.getNotification().isBubbleNotification(), + record.getProposedImportance() ); rankings.add(ranking); } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index cbaf485c077f..d3443066155f 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -210,6 +210,7 @@ public final class NotificationRecord { // Whether this notification record should have an update logged the next time notifications // are sorted. private boolean mPendingLogUpdate = false; + private int mProposedImportance = IMPORTANCE_UNSPECIFIED; public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { @@ -499,6 +500,8 @@ public final class NotificationRecord { pw.println(prefix + "mImportance=" + NotificationListenerService.Ranking.importanceToString(mImportance)); pw.println(prefix + "mImportanceExplanation=" + getImportanceExplanation()); + pw.println(prefix + "mProposedImportance=" + + NotificationListenerService.Ranking.importanceToString(mProposedImportance)); pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked); pw.println(prefix + "mIntercept=" + mIntercept); pw.println(prefix + "mHidden==" + mHidden); @@ -738,6 +741,12 @@ public final class NotificationRecord { Adjustment.KEY_NOT_CONVERSATION, Boolean.toString(mIsNotConversationOverride)); } + if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) { + mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL); + EventLogTags.writeNotificationAdjusted(getKey(), + Adjustment.KEY_IMPORTANCE_PROPOSAL, + Integer.toString(mProposedImportance)); + } if (!signals.isEmpty() && adjustment.getIssuer() != null) { mAdjustmentIssuer = adjustment.getIssuer(); } @@ -870,6 +879,10 @@ public final class NotificationRecord { return stats.naturalImportance; } + public int getProposedImportance() { + return mProposedImportance; + } + public float getRankingScore() { return mRankingScore; } diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java new file mode 100644 index 000000000000..6dc9029f8928 --- /dev/null +++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.notification; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.service.notification.SnoozeCriterion; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * Class that stores any field in a NotificationRecord that can change via an extractor. + * Used to cache previous data used in a sort. + */ +public final class NotificationRecordExtractorData { + private final int mPosition; + private final int mVisibility; + private final boolean mShowBadge; + private final boolean mAllowBubble; + private final boolean mIsBubble; + private final NotificationChannel mChannel; + private final String mGroupKey; + private final ArrayList<String> mOverridePeople; + private final ArrayList<SnoozeCriterion> mSnoozeCriteria; + private final Integer mUserSentiment; + private final Integer mSuppressVisually; + private final ArrayList<Notification.Action> mSystemSmartActions; + private final ArrayList<CharSequence> mSmartReplies; + private final int mImportance; + + // These fields may not trigger a reranking but diffs here may be logged. + private final float mRankingScore; + private final boolean mIsConversation; + private final int mProposedImportance; + + NotificationRecordExtractorData(int position, int visibility, boolean showBadge, + boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey, + ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, + Integer userSentiment, Integer suppressVisually, + ArrayList<Notification.Action> systemSmartActions, + ArrayList<CharSequence> smartReplies, int importance, float rankingScore, + boolean isConversation, int proposedImportance) { + mPosition = position; + mVisibility = visibility; + mShowBadge = showBadge; + mAllowBubble = allowBubble; + mIsBubble = isBubble; + mChannel = channel; + mGroupKey = groupKey; + mOverridePeople = overridePeople; + mSnoozeCriteria = snoozeCriteria; + mUserSentiment = userSentiment; + mSuppressVisually = suppressVisually; + mSystemSmartActions = systemSmartActions; + mSmartReplies = smartReplies; + mImportance = importance; + mRankingScore = rankingScore; + mIsConversation = isConversation; + mProposedImportance = proposedImportance; + } + + // Returns whether the provided NotificationRecord differs from the cached data in any way. + // Should be guarded by mNotificationLock; not annotated here as this class is static. + boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) { + return mPosition != newPosition + || mVisibility != r.getPackageVisibilityOverride() + || mShowBadge != r.canShowBadge() + || mAllowBubble != r.canBubble() + || mIsBubble != r.getNotification().isBubbleNotification() + || !Objects.equals(mChannel, r.getChannel()) + || !Objects.equals(mGroupKey, r.getGroupKey()) + || !Objects.equals(mOverridePeople, r.getPeopleOverride()) + || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) + || !Objects.equals(mUserSentiment, r.getUserSentiment()) + || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects()) + || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) + || !Objects.equals(mSmartReplies, r.getSmartReplies()) + || mImportance != r.getImportance() + || mProposedImportance != r.getProposedImportance(); + } + + // Returns whether the NotificationRecord has a change from this data for which we should + // log an update. This method specifically targets fields that may be changed via + // adjustments from the assistant. + // + // Fields here are the union of things in NotificationRecordLogger.shouldLogReported + // and NotificationRecord.applyAdjustments. + // + // Should be guarded by mNotificationLock; not annotated here as this class is static. + boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) { + return mPosition != newPosition + || !Objects.equals(mChannel, r.getChannel()) + || !Objects.equals(mGroupKey, r.getGroupKey()) + || !Objects.equals(mOverridePeople, r.getPeopleOverride()) + || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) + || !Objects.equals(mUserSentiment, r.getUserSentiment()) + || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) + || !Objects.equals(mSmartReplies, r.getSmartReplies()) + || mImportance != r.getImportance() + || !r.rankingScoreMatches(mRankingScore) + || mIsConversation != r.isConversation() + || mProposedImportance != r.getProposedImportance(); + } +} diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 78dad124a4c1..7ca9ac75a939 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -108,7 +108,7 @@ public class PreferencesHelper implements RankingConfig { @VisibleForTesting static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000; @VisibleForTesting - static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 50000; + static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000; private static final int NOTIFICATION_PREFERENCES_PULL_LIMIT = 1000; private static final int NOTIFICATION_CHANNEL_PULL_LIMIT = 2000; @@ -1007,6 +1007,7 @@ public class PreferencesHelper implements RankingConfig { channel.setAllowBubbles(existing != null ? existing.getAllowBubbles() : NotificationChannel.DEFAULT_ALLOW_BUBBLE); + channel.setImportantConversation(false); } clearLockedFieldsLocked(channel); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 6d63891b9fba..89a920ab604b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -91,6 +91,7 @@ import android.security.GateKeeper; import android.service.gatekeeper.IGateKeeperService; import android.service.voice.VoiceInteractionManagerInternal; import android.stats.devicepolicy.DevicePolicyEnums; +import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -1763,6 +1764,63 @@ public class UserManagerService extends IUserManager.Stub { } } + /** + * Returns whether switching users is currently allowed for the provided user. + * <p> + * Switching users is not allowed in the following cases: + * <li>the user is in a phone call</li> + * <li>{@link UserManager#DISALLOW_USER_SWITCH} is set</li> + * <li>system user hasn't been unlocked yet</li> + * + * @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is + * switchable. + */ + public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) { + checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability"); + + final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); + t.traceBegin("getUserSwitchability-" + userId); + + int flags = UserManager.SWITCHABILITY_STATUS_OK; + + t.traceBegin("TM.isInCall"); + final long identity = Binder.clearCallingIdentity(); + try { + final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class); + if (telecomManager != null && telecomManager.isInCall()) { + flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + t.traceEnd(); + + t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH"); + if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) { + flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED; + } + t.traceEnd(); + + // System User is always unlocked in Headless System User Mode, so ignore this flag + if (!UserManager.isHeadlessSystemUserMode()) { + t.traceBegin("getInt-ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED"); + final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0; + t.traceEnd(); + t.traceBegin("isUserUnlocked-USER_SYSTEM"); + final boolean systemUserUnlocked = mLocalService.isUserUnlocked(UserHandle.USER_SYSTEM); + t.traceEnd(); + + if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) { + flags |= UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED; + } + } + t.traceEnd(); + + return flags; + } + @Override public boolean isUserSwitcherEnabled(@UserIdInt int mUserId) { boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(), 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 554e2690b878..20c9a211e586 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -4215,7 +4215,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } boolean changed = false; - Set<Permission> needsUpdate = null; synchronized (mLock) { final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator(); while (it.hasNext()) { @@ -4234,26 +4233,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt + " that used to be declared by " + bp.getPackageName()); it.remove(); } - if (needsUpdate == null) { - needsUpdate = new ArraySet<>(); - } - needsUpdate.add(bp); - } - } - if (needsUpdate != null) { - for (final Permission bp : needsUpdate) { - final AndroidPackage sourcePkg = - mPackageManagerInt.getPackage(bp.getPackageName()); - final PackageStateInternal sourcePs = - mPackageManagerInt.getPackageStateInternal(bp.getPackageName()); - synchronized (mLock) { - if (sourcePkg != null && sourcePs != null) { - continue; - } - Slog.w(TAG, "Removing dangling permission tree: " + bp.getName() - + " from package " + bp.getPackageName()); - mRegistry.removePermission(bp.getName()); - } } } return changed; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index c8a11a529c30..5f705ff7cfd4 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4152,9 +4152,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_DEMO_APP_2: case KeyEvent.KEYCODE_DEMO_APP_3: case KeyEvent.KEYCODE_DEMO_APP_4: { - // TODO(b/254604589): Dispatch KeyEvent to System UI. - sendSystemKeyToStatusBarAsync(keyCode); - // Just drop if keys are not intercepted for direct key. result &= ~ACTION_PASS_TO_USER; break; diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index e7221c850da8..fde0b34c7836 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -191,4 +191,12 @@ public interface StatusBarManagerInternal { * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp */ void enterStageSplitFromRunningApp(boolean leftOrTop); + + /** + * Shows the media output switcher dialog. + * + * @param packageName of the session for which the output switcher is shown. + * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher + */ + void showMediaOutputSwitcher(String packageName); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 45748e6ce76d..ab7292d49c7d 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -714,6 +714,16 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } catch (RemoteException ex) { } } } + + @Override + public void showMediaOutputSwitcher(String packageName) { + if (mBar != null) { + try { + mBar.showMediaOutputSwitcher(packageName); + } catch (RemoteException ex) { + } + } + } }; private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 4cbb2cd37049..58b9e3e2aab0 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4724,6 +4724,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mTaskChangeNotificationController.notifyTaskFocusChanged(prevTask.mTaskId, false); } mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true); + mTaskSupervisor.mRecentTasks.add(task); } applyUpdateLockStateLocked(r); diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index fd6c9743cb6b..b160af6a3e11 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -98,7 +98,7 @@ class AppTaskImpl extends IAppTask.Stub { throw new IllegalArgumentException("Unable to find task ID " + mTaskId); } return mService.getRecentTasks().createRecentTaskInfo(task, - false /* stripExtras */); + false /* stripExtras */, true /* getTasksAllowed */); } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index abaa3630ff7b..0ea6157dd2a4 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -892,7 +892,7 @@ public class AppTransitionController { * * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled. */ - private static boolean isTaskViewTask(WindowContainer wc) { + static boolean isTaskViewTask(WindowContainer wc) { // We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and // it is not guaranteed to work this logic in the future version. return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer; diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index ba0413df6325..c6037dab6568 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -203,8 +203,11 @@ final class DisplayRotationCompatPolicy { || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) { return; } - boolean cycleThroughStop = mWmService.mLetterboxConfiguration - .isCameraCompatRefreshCycleThroughStopEnabled(); + boolean cycleThroughStop = + mWmService.mLetterboxConfiguration + .isCameraCompatRefreshCycleThroughStopEnabled() + && !activity.mLetterboxUiController + .shouldRefreshActivityViaPauseForCameraCompat(); try { activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); ProtoLog.v(WM_DEBUG_STATES, @@ -255,7 +258,8 @@ final class DisplayRotationCompatPolicy { Configuration lastReportedConfig) { return newConfig.windowConfiguration.getDisplayRotation() != lastReportedConfig.windowConfiguration.getDisplayRotation() - && isTreatmentEnabledForActivity(activity); + && isTreatmentEnabledForActivity(activity) + && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat(); } /** @@ -294,7 +298,8 @@ final class DisplayRotationCompatPolicy { // handle dynamic changes so we shouldn't force rotate them. && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED - && mCameraIdPackageBiMap.containsPackageName(activity.packageName); + && mCameraIdPackageBiMap.containsPackageName(activity.packageName) + && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } private synchronized void notifyCameraOpened( diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 0c8a6453e6fb..9c43c1d62ab8 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,12 +17,18 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ActivityInfo.screenOrientationToString; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; @@ -132,6 +138,15 @@ final class LetterboxUiController { @Nullable private Letterbox mLetterbox; + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowForceRotation; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowRefresh; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause; + // Whether activity "refresh" was requested but not finished in // ActivityRecord#activityResumedLocked following the camera compat force rotation in // DisplayRotationCompatPolicy. @@ -154,8 +169,33 @@ final class LetterboxUiController { readComponentProperty(packageManager, mActivityRecord.packageName, mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled, PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION); + mBooleanPropertyCameraCompatAllowForceRotation = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION); + mBooleanPropertyCameraCompatAllowRefresh = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH); + mBooleanPropertyCameraCompatEnableRefreshViaPause = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE); } + /** + * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code + * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the + * property isn't specified for the package. + * + * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the + * property is unset. Particularly, when this returns {@code null}, {@link + * #shouldEnableWithOverrideAndProperty} will check the value of override for the final + * decision. + */ @Nullable private static Boolean readComponentProperty(PackageManager packageManager, String packageName, BooleanSupplier gatingCondition, String propertyName) { @@ -210,15 +250,11 @@ final class LetterboxUiController { * </ul> */ boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) { - if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) { - return false; - } - if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) { - return false; - } - if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation) - && !mActivityRecord.info.isChangeEnabled( - OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) { + if (!shouldEnableWithOverrideAndProperty( + /* gatingCondition */ mLetterboxConfiguration + ::isPolicyForIgnoringRequestedOrientationEnabled, + OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION, + mBooleanPropertyIgnoreRequestedOrientation)) { return false; } if (mIsRelauchingAfterRequestedOrientationChanged) { @@ -262,6 +298,109 @@ final class LetterboxUiController { mIsRefreshAfterRotationRequested = isRequested; } + /** + * Whether activity is eligible for activity "refresh" after camera compat force rotation + * treatment. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH, + mBooleanPropertyCameraCompatAllowRefresh); + } + + /** + * Whether activity should be "refreshed" after the camera compat force rotation treatment + * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped + * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the + * component property by the app developers. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device + * manufacturer with override / by the app developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityViaPauseForCameraCompat() { + return shouldEnableWithOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, + mBooleanPropertyCameraCompatEnableRefreshViaPause); + } + + /** + * Whether activity is eligible for camera compat force rotation treatment. See {@link + * DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldForceRotateForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION, + mBooleanPropertyCameraCompatAllowForceRotation); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>OEM didn't opt out with a {@code overrideChangeId} override + * <li>App developers didn't opt out with a component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled based with the heuristic but can be + * disabled on per-app basis by OEMs or app developers. + */ + private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + return !Boolean.FALSE.equals(property) + && !mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>App developers didn't opt out with a component {@code property} + * <li>App developers opted in with a component {@code property} or an OEM opted in with a + * component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled only on per-app basis. + */ + private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + if (Boolean.FALSE.equals(property)) { + return false; + } + return Boolean.TRUE.equals(property) + || mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + boolean hasWallpaperBackgroundForLetterbox() { return mShowWallpaperForLetterboxBackground; } @@ -1007,10 +1146,9 @@ final class LetterboxUiController { final ActivityRecord firstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity( ActivityRecord::fillsParent, mActivityRecord, false /* includeBoundary */, true /* traverseTopToBottom */); - if (firstOpaqueActivityBeneath == null - || mActivityRecord.launchedFromUid != firstOpaqueActivityBeneath.getUid()) { + if (firstOpaqueActivityBeneath == null) { // We skip letterboxing if the translucent activity doesn't have any opaque - // activities beneath of if it's launched from a different user (e.g. notification) + // activities beneath return; } inheritConfiguration(firstOpaqueActivityBeneath); diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 4860762a5f7f..1fc061b2ca78 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -976,7 +976,7 @@ class RecentTasks { continue; } - res.add(createRecentTaskInfo(task, true /* stripExtras */)); + res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed)); } return res; } @@ -1895,7 +1895,8 @@ class RecentTasks { /** * Creates a new RecentTaskInfo from a Task. */ - ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) { + ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras, + boolean getTasksAllowed) { final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); // If the recent Task is detached, we consider it will be re-attached to the default // TaskDisplayArea because we currently only support recent overview in the default TDA. @@ -1907,6 +1908,9 @@ class RecentTasks { rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID; rti.persistentId = rti.taskId; rti.lastSnapshotData.set(tr.mLastTaskSnapshotData); + if (!getTasksAllowed) { + Task.trimIneffectiveInfo(tr, rti); + } // Fill in organized child task info for the task created by organizer. if (tr.mCreatedByOrganizer) { diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java index 33f019e0c9fb..4e339f1867ae 100644 --- a/services/core/java/com/android/server/wm/RunningTasks.java +++ b/services/core/java/com/android/server/wm/RunningTasks.java @@ -177,6 +177,10 @@ class RunningTasks { } // Fill in some deprecated values rti.id = rti.taskId; + + if (!mAllowed) { + Task.trimIneffectiveInfo(task, rti); + } return rti; } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ba89657b2e29..22033e17cbff 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -609,7 +609,7 @@ class Task extends TaskFragment { */ ActivityRecord mChildPipActivity; - boolean mLastSurfaceShowing = true; + boolean mLastSurfaceShowing; /** * Tracks if a back gesture is in progress. @@ -3468,6 +3468,54 @@ class Task extends TaskFragment { info.isSleeping = shouldSleepActivities(); } + /** + * Removes the activity info if the activity belongs to a different uid, which is + * different from the app that hosts the task. + */ + static void trimIneffectiveInfo(Task task, TaskInfo info) { + final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing, + false /* traverseTopToBottom */); + final int baseActivityUid = + baseActivity != null ? baseActivity.getUid() : task.effectiveUid; + + if (info.topActivityInfo != null + && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) { + // Making a copy to prevent eliminating the info in the original ActivityRecord. + info.topActivityInfo = new ActivityInfo(info.topActivityInfo); + info.topActivityInfo.applicationInfo = + new ApplicationInfo(info.topActivityInfo.applicationInfo); + + // Strip the sensitive info. + info.topActivity = new ComponentName("", ""); + info.topActivityInfo.packageName = ""; + info.topActivityInfo.taskAffinity = ""; + info.topActivityInfo.processName = ""; + info.topActivityInfo.name = ""; + info.topActivityInfo.parentActivityName = ""; + info.topActivityInfo.targetActivity = ""; + info.topActivityInfo.splitName = ""; + info.topActivityInfo.applicationInfo.className = ""; + info.topActivityInfo.applicationInfo.credentialProtectedDataDir = ""; + info.topActivityInfo.applicationInfo.dataDir = ""; + info.topActivityInfo.applicationInfo.deviceProtectedDataDir = ""; + info.topActivityInfo.applicationInfo.manageSpaceActivityName = ""; + info.topActivityInfo.applicationInfo.nativeLibraryDir = ""; + info.topActivityInfo.applicationInfo.nativeLibraryRootDir = ""; + info.topActivityInfo.applicationInfo.processName = ""; + info.topActivityInfo.applicationInfo.publicSourceDir = ""; + info.topActivityInfo.applicationInfo.scanPublicSourceDir = ""; + info.topActivityInfo.applicationInfo.scanSourceDir = ""; + info.topActivityInfo.applicationInfo.sourceDir = ""; + info.topActivityInfo.applicationInfo.taskAffinity = ""; + info.topActivityInfo.applicationInfo.name = ""; + info.topActivityInfo.applicationInfo.packageName = ""; + } + + if (task.effectiveUid != baseActivityUid) { + info.baseActivity = new ComponentName("", ""); + } + } + @Nullable PictureInPictureParams getPictureInPictureParams() { final Task topTask = getTopMostTask(); if (topTask == null) return null; @@ -4150,13 +4198,7 @@ class Task extends TaskFragment { @Override boolean showSurfaceOnCreation() { - if (mCreatedByOrganizer) { - // Tasks created by the organizer are default visible because they can synchronously - // update the leash before new children are added to the task. - return true; - } - // Organized tasks handle their own surface visibility - return !canBeOrganized(); + return false; } @Override @@ -6364,6 +6406,11 @@ class Task extends TaskFragment { return this; } + Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) { + mRemoveWithTaskOrganizer = removeWithTaskOrganizer; + return this; + } + private Builder setUserId(int userId) { mUserId = userId; return this; @@ -6561,7 +6608,7 @@ class Task extends TaskFragment { mCallingPackage = mActivityInfo.packageName; mResizeMode = mActivityInfo.resizeMode; mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture(); - if (mActivityOptions != null) { + if (!mRemoveWithTaskOrganizer && mActivityOptions != null) { mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer(); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 90a0dffa25f2..49b2a4ef51a7 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -540,9 +540,12 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr synchronized (mGlobalLock) { final TaskFragmentOrganizerState organizerState = mTaskFragmentOrganizerState.get(organizer.asBinder()); - return organizerState != null - ? organizerState.mRemoteAnimationDefinition - : null; + if (organizerState == null) { + Slog.e(TAG, "TaskFragmentOrganizer has been unregistered or died when trying" + + " to play animation on its organized windows."); + return null; + } + return organizerState.mRemoteAnimationDefinition; } } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index d619547dbbd1..d780cae9e845 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -783,7 +783,8 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } @Override - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { enforceTaskPermission("createRootTask()"); final long origId = Binder.clearCallingIdentity(); try { @@ -795,7 +796,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return; } - createRootTask(display, windowingMode, launchCookie); + createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer); } } finally { Binder.restoreCallingIdentity(origId); @@ -804,6 +805,12 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { @VisibleForTesting Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) { + return createRootTask(display, windowingMode, launchCookie, + false /* removeWithTaskOrganizer */); + } + + Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d", display.mDisplayId, windowingMode); // We want to defer the task appear signal until the task is fully created and attached to @@ -816,6 +823,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { .setDeferTaskAppear(true) .setLaunchCookie(launchCookie) .setParent(display.getDefaultTaskDisplayArea()) + .setRemoveWithTaskOrganizer(removeWithTaskOrganizer) .build(); task.setDeferTaskAppear(false /* deferTaskAppear */); return task; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 1d25dbc0d533..b2dab78b993d 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -371,7 +371,7 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. - final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds(); + final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); // Full size of the wallpaper (usually larger than bounds above to parallax scroll when // swiping through Launcher pages). final Rect wallpaperFrame = wallpaperWin.getFrame(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index fb584feeaf04..8bdab9c22ab7 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3197,11 +3197,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, boolean isVoiceInteraction) { - if (isOrganized() + if (AppTransitionController.isTaskViewTask(this) || (isOrganized() // TODO(b/161711458): Clean-up when moved to shell. && getWindowingMode() != WINDOWING_MODE_FULLSCREEN && getWindowingMode() != WINDOWING_MODE_FREEFORM - && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) { + && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) { return null; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 3187337de051..fd477532e984 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1949,12 +1949,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub creationParams.getPairedPrimaryFragmentToken()); final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment); position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP; + } else if (creationParams.getPairedActivityToken() != null) { + // When there is a paired Activity, we want to place the new TaskFragment right above + // the paired Activity to make sure the Activity position is not changed after reparent. + final ActivityRecord pairedActivity = ActivityRecord.forTokenLocked( + creationParams.getPairedActivityToken()); + final int pairedPosition = ownerTask.mChildren.indexOf(pairedActivity); + position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP; } else { position = POSITION_TOP; } ownerTask.addChild(taskFragment, position); taskFragment.setWindowingMode(creationParams.getWindowingMode()); taskFragment.setBounds(creationParams.getInitialBounds()); + // Record the initial relative embedded bounds. + taskFragment.updateRelativeEmbeddedBounds(); mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment); if (transition != null) transition.collectExistenceChange(taskFragment); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 04699619bfc8..ae03fbbe1cff 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -396,14 +396,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP int mPrepareSyncSeqId = 0; /** - * {@code true} when the client was still drawing for sync when the sync-set was finished or - * cancelled. This can happen if the window goes away during a sync. In this situation we need - * to make sure to still apply the postDrawTransaction when it finishes to prevent the client - * from getting stuck in a bad state. - */ - boolean mClientWasDrawingForSync = false; - - /** * Special mode that is intended only for the rounded corner overlay: during rotation * transition, we un-rotate the window token such that the window appears as it did before the * rotation. @@ -3081,12 +3073,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mLastReportedConfiguration.getMergedConfiguration(); } - /** Returns the last window configuration bounds reported to the client. */ - Rect getLastReportedBounds() { - final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds(); - return !bounds.isEmpty() ? bounds : getBounds(); - } - void adjustStartingWindowFlags() { if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null && mActivityRecord.mStartingWindow != null) { @@ -4421,6 +4407,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.print("null"); } + if (mXOffset != 0 || mYOffset != 0) { + pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset); + } if (mHScale != 1 || mVScale != 1) { pw.println(prefix + "mHScale=" + mHScale + " mVScale=" + mVScale); @@ -5573,7 +5562,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSurfacePosition); if (mWallpaperScale != 1f) { - final Rect bounds = getLastReportedBounds(); + final Rect bounds = getParentFrame(); Matrix matrix = mTmpMatrix; matrix.setTranslate(mXOffset, mYOffset); matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(), @@ -5686,6 +5675,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP && imeTarget.compareTo(this) <= 0; return inTokenWithAndAboveImeTarget; } + + // The condition is for the system dialog not belonging to any Activity. + // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but + // should be placed above the IME window. + if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) + == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) { + return true; + } return false; } @@ -6019,9 +6016,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override void finishSync(Transaction outMergedTransaction, boolean cancel) { - if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) { - mClientWasDrawingForSync = true; - } mPrepareSyncSeqId = 0; if (cancel) { // This is leaving sync so any buffers left in the sync have a chance of @@ -6089,9 +6083,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP layoutNeeded = onSyncFinishedDrawing(); } - layoutNeeded |= - mWinAnimator.finishDrawingLocked(postDrawTransaction, mClientWasDrawingForSync); - mClientWasDrawingForSync = false; + layoutNeeded |= mWinAnimator.finishDrawingLocked(postDrawTransaction); // We always want to force a traversal after a finish draw for blast sync. return !skipLayout && (hasSyncHandlers || layoutNeeded); } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index a0ba8fda906f..f3642480da58 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -151,17 +151,6 @@ class WindowStateAnimator { int mAttrType; - /** - * Handles surface changes synchronized to after the client has drawn the surface. This - * transaction is currently used to reparent the old surface children to the new surface once - * the client has completed drawing to the new surface. - * This transaction is also used to merge transactions parceled in by the client. The client - * uses the transaction to update the relative z of its children from the old parent surface - * to the new parent surface once window manager reparents its children. - */ - private final SurfaceControl.Transaction mPostDrawTransaction = - new SurfaceControl.Transaction(); - WindowStateAnimator(final WindowState win) { final WindowManagerService service = win.mWmService; @@ -217,8 +206,7 @@ class WindowStateAnimator { } } - boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction, - boolean forceApplyNow) { + boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction) { final boolean startingWindow = mWin.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; if (startingWindow) { @@ -240,14 +228,7 @@ class WindowStateAnimator { } if (postDrawTransaction != null) { - // If there is no surface, the last draw was for the previous surface. We don't want to - // wait until the new surface is shown and instead just apply the transaction right - // away. - if (mLastHidden && mDrawState != NO_SURFACE && !forceApplyNow) { - mPostDrawTransaction.merge(postDrawTransaction); - } else { - mWin.getSyncTransaction().merge(postDrawTransaction); - } + mWin.getSyncTransaction().merge(postDrawTransaction); layoutNeeded = true; } @@ -547,7 +528,6 @@ class WindowStateAnimator { if (!shown) return false; - t.merge(mPostDrawTransaction); return true; } @@ -714,10 +694,6 @@ class WindowStateAnimator { } void destroySurface(SurfaceControl.Transaction t) { - // Since the SurfaceControl is getting torn down, it's safe to just clean up any - // pending transactions that were in mPostDrawTransaction, as well. - t.merge(mPostDrawTransaction); - try { if (mSurfaceController != null) { mSurfaceController.destroy(t); diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index f628fbad892d..abe48f894e59 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -464,6 +464,14 @@ </xs:complexType> <xs:complexType name="refreshRateConfigs"> + <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + <xs:element name="defaultPeakRefreshRate" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> <xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig" minOccurs="0" maxOccurs="1"> <xs:annotation name="final"/> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index cb081791ffce..2c97af55f092 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -186,8 +186,12 @@ package com.android.server.display.config { public class RefreshRateConfigs { ctor public RefreshRateConfigs(); + method public final java.math.BigInteger getDefaultPeakRefreshRate(); + method public final java.math.BigInteger getDefaultRefreshRate(); method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs(); method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs(); + method public final void setDefaultPeakRefreshRate(java.math.BigInteger); + method public final void setDefaultRefreshRate(java.math.BigInteger); method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig); method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig); } diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java index 8a3a44ae9f35..0993295e162f 100644 --- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.WorkerThread; import android.content.Context; import android.database.Cursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; @@ -149,6 +150,8 @@ class ContactsQueryHelper { found = true; } + } catch (SQLiteException exception) { + Slog.w("SQLite exception when querying contacts.", exception); } if (found && lookupKey != null && hasPhoneNumber) { return queryPhoneNumber(lookupKey); diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 693f3a0cf8a0..eff9e8da9a76 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -271,22 +271,22 @@ public class DataManager { private ConversationChannel getConversationChannel(String packageName, int userId, String shortcutId, ConversationInfo conversationInfo) { ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId); - return getConversationChannel(shortcutInfo, conversationInfo); + return getConversationChannel( + shortcutInfo, conversationInfo, packageName, userId, shortcutId); } @Nullable private ConversationChannel getConversationChannel(ShortcutInfo shortcutInfo, - ConversationInfo conversationInfo) { + ConversationInfo conversationInfo, String packageName, int userId, String shortcutId) { if (conversationInfo == null || conversationInfo.isDemoted()) { return null; } if (shortcutInfo == null) { - Slog.e(TAG, " Shortcut no longer found"); + Slog.e(TAG, "Shortcut no longer found"); + mInjector.getBackgroundExecutor().execute( + () -> removeConversations(packageName, userId, Set.of(shortcutId))); return null; } - String packageName = shortcutInfo.getPackage(); - String shortcutId = shortcutInfo.getId(); - int userId = shortcutInfo.getUserId(); int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); NotificationChannel parentChannel = mNotificationManagerInternal.getNotificationChannel(packageName, uid, @@ -1130,38 +1130,41 @@ public class DataManager { public void onShortcutsRemoved(@NonNull String packageName, @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { mInjector.getBackgroundExecutor().execute(() -> { - int uid = Process.INVALID_UID; - try { - uid = mContext.getPackageManager().getPackageUidAsUser( - packageName, user.getIdentifier()); - } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Package not found: " + packageName, e); - } - PackageData packageData = getPackage(packageName, user.getIdentifier()); - Set<String> shortcutIds = new HashSet<>(); + HashSet<String> shortcutIds = new HashSet<>(); for (ShortcutInfo shortcutInfo : shortcuts) { - if (packageData != null) { - if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutInfo.getId()); - packageData.deleteDataForConversation(shortcutInfo.getId()); - } shortcutIds.add(shortcutInfo.getId()); } - if (uid != Process.INVALID_UID) { - mNotificationManagerInternal.onConversationRemoved( - packageName, uid, shortcutIds); - } + removeConversations(packageName, user.getIdentifier(), shortcutIds); }); } } + private void removeConversations( + @NonNull String packageName, @NonNull int userId, @NonNull Set<String> shortcutIds) { + PackageData packageData = getPackage(packageName, userId); + if (packageData != null) { + for (String shortcutId : shortcutIds) { + if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutId); + packageData.deleteDataForConversation(shortcutId); + } + } + try { + int uid = mContext.getPackageManager().getPackageUidAsUser( + packageName, userId); + mNotificationManagerInternal.onConversationRemoved(packageName, uid, shortcutIds); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Package not found when removing conversation: " + packageName, e); + } + } + /** Listener for the notifications and their settings changes. */ private class NotificationListener extends NotificationListenerService { private final int mUserId; - // Conversation package name + shortcut ID -> Number of active notifications + // Conversation package name + shortcut ID -> Keys of active notifications @GuardedBy("this") - private final Map<Pair<String, String>, Integer> mActiveNotifCounts = new ArrayMap<>(); + private final Map<Pair<String, String>, Set<String>> mActiveNotifKeys = new ArrayMap<>(); private NotificationListener(int userId) { mUserId = userId; @@ -1175,8 +1178,10 @@ public class DataManager { String shortcutId = sbn.getNotification().getShortcutId(); PackageData packageData = getPackageIfConversationExists(sbn, conversationInfo -> { synchronized (this) { - mActiveNotifCounts.merge( - Pair.create(sbn.getPackageName(), shortcutId), 1, Integer::sum); + Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent( + Pair.create(sbn.getPackageName(), shortcutId), + (unusedKey) -> new HashSet<>()); + notificationKeys.add(sbn.getKey()); } }); @@ -1215,12 +1220,12 @@ public class DataManager { Pair<String, String> conversationKey = Pair.create(sbn.getPackageName(), shortcutId); synchronized (this) { - int count = mActiveNotifCounts.getOrDefault(conversationKey, 0) - 1; - if (count <= 0) { - mActiveNotifCounts.remove(conversationKey); + Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent( + conversationKey, (unusedKey) -> new HashSet<>()); + notificationKeys.remove(sbn.getKey()); + if (notificationKeys.isEmpty()) { + mActiveNotifKeys.remove(conversationKey); cleanupCachedShortcuts(mUserId, MAX_CACHED_RECENT_SHORTCUTS); - } else { - mActiveNotifCounts.put(conversationKey, count); } } }); @@ -1286,7 +1291,7 @@ public class DataManager { } synchronized boolean hasActiveNotifications(String packageName, String shortcutId) { - return mActiveNotifCounts.containsKey(Pair.create(packageName, shortcutId)); + return mActiveNotifKeys.containsKey(Pair.create(packageName, shortcutId)); } } @@ -1349,9 +1354,11 @@ public class DataManager { } private void updateConversationStoreThenNotifyListeners(ConversationStore cs, - ConversationInfo modifiedConv, ShortcutInfo shortcutInfo) { + ConversationInfo modifiedConv, @NonNull ShortcutInfo shortcutInfo) { cs.addOrUpdate(modifiedConv); - ConversationChannel channel = getConversationChannel(shortcutInfo, modifiedConv); + ConversationChannel channel = getConversationChannel( + shortcutInfo, modifiedConv, shortcutInfo.getPackage(), shortcutInfo.getUserId(), + shortcutInfo.getId()); if (channel != null) { notifyConversationsListeners(Arrays.asList(channel)); } diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index dad9fe8648b2..31599eed539d 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -74,7 +74,7 @@ public class AudioDeviceBrokerTest { mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem)); mSpySystemServer = spy(new NoOpSystemServerAdapter()); mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory, - mSpySystemServer); + mSpySystemServer, mSpyAudioSystem); mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 666d4010e921..3c735e335e75 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -41,7 +41,6 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; -import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -55,7 +54,6 @@ import android.testing.TestableContext; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.R; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.CallbackWithProbe; @@ -369,274 +367,6 @@ public class FingerprintAuthenticationClientTest { verify(mCancellationSignal).cancel(); } - @Test - public void fingerprintPowerIgnoresAuthInWindow() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onPowerPressed(); - client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(false)); - verify(mCancellationSignal).cancel(); - } - - @Test - public void fingerprintAuthIgnoredWaitingForPower() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onAuthenticated(new Fingerprint("friendly", 3 /* fingerId */, 4 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onPowerPressed(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(false)); - verify(mCancellationSignal).cancel(); - } - - @Test - public void fingerprintAuthFailsWhenAuthAfterPower() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onPowerPressed(); - mLooper.dispatchAll(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), eq(true)); - verify(mCallback).onClientFinished(any(), eq(false)); - when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal); - } - - @Test - public void sideFingerprintDoesntSendAuthImmediately() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - } - - @Test - public void sideFingerprintSkipsWindowIfFingerUp() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FINGER_UP, 0); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - final int vendorAcquireMessage = 1234; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, - FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, - vendorAcquireMessage); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - final int vendorAcquireMessage = 1234; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, - FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, - vendorAcquireMessage); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - } - - @Test - public void sideFingerprintSendsAuthIfFingerUp() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAcquired(FINGER_UP, 0); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintShortCircuitExpires() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final int timeBeforeAuthSent = 500; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsKeyguardPowerPressWindow, timeBeforeAuthSent); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAcquired(FINGER_UP, 0); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(500); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - mLooper.moveTimeForward(500); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception { - final int powerWindow = 500; - final long authStart = 300; - - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsBpPowerPressWindow, powerWindow); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - - // Acquire start occurs at time = 0ms - when(mClock.millis()).thenReturn(0L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // Auth occurs at time = 300 - when(mClock.millis()).thenReturn(authStart); - // At this point the delay should be 500 - (300 - 0) == 200 milliseconds. - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - // After waiting 200 milliseconds, auth should succeed. - mLooper.moveTimeForward(powerWindow - authStart); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception { - final int powerWindow = 500; - - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsBpPowerPressWindow, powerWindow); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - // Acquire start occurs at time = 0ms - when(mClock.millis()).thenReturn(0L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // Auth reject occurs at time = 300ms - when(mClock.millis()).thenReturn(300L); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - false /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(300); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - when(mClock.millis()).thenReturn(1300L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // If code is correct, the new acquired start timestamp should be used - // and the code should only have to wait 500 - (1500-1300)ms. - when(mClock.millis()).thenReturn(1500L); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(299); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - mLooper.moveTimeForward(1); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFpsPowerPressCancelsIsntantly() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - - client.onPowerPressed(); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), eq(true)); - verify(mCallback).onClientFinished(any(), eq(false)); - } - private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 86c59379a61e..77e5d1d60cb5 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -51,6 +51,8 @@ import java.nio.file.Path; public final class DisplayDeviceConfigTest { private static final int DEFAULT_PEAK_REFRESH_RATE = 75; private static final int DEFAULT_REFRESH_RATE = 120; + private static final int DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE = 55; + private static final int DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE = 95; private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30}; private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21}; private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160}; @@ -150,8 +152,10 @@ public final class DisplayDeviceConfigTest { assertEquals("ProximitySensor123", mDisplayDeviceConfig.getProximitySensor().name); assertEquals("prox_type_1", mDisplayDeviceConfig.getProximitySensor().type); - assertEquals(75, mDisplayDeviceConfig.getDefaultLowRefreshRate()); - assertEquals(90, mDisplayDeviceConfig.getDefaultHighRefreshRate()); + assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()); + assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()); + assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate()); + assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate()); assertArrayEquals(new int[]{45, 55}, mDisplayDeviceConfig.getLowDisplayBrightnessThresholds()); assertArrayEquals(new int[]{50, 60}, @@ -230,8 +234,12 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA); assertArrayEquals(new float[]{29, 30, 31}, mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA); - assertEquals(mDisplayDeviceConfig.getDefaultLowRefreshRate(), DEFAULT_REFRESH_RATE); - assertEquals(mDisplayDeviceConfig.getDefaultHighRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(), + DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate(), + DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE); assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(), LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(), @@ -449,6 +457,8 @@ public final class DisplayDeviceConfigTest { + "<type>prox_type_1</type>\n" + "</proxSensor>\n" + "<refreshRate>\n" + + "<defaultRefreshRate>45</defaultRefreshRate>\n" + + "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n" + "<lowerBlockingZoneConfigs>\n" + "<defaultRefreshRate>75</defaultRefreshRate>\n" + "<blockingZoneThreshold>\n" @@ -550,10 +560,14 @@ public final class DisplayDeviceConfigTest { .thenReturn(new int[]{370, 380, 390}); // Configs related to refresh rates and blocking zones - when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) + when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate)) .thenReturn(DEFAULT_PEAK_REFRESH_RATE); - when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate)) + when(mResources.getInteger(R.integer.config_defaultRefreshRate)) .thenReturn(DEFAULT_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + .thenReturn(DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone)) + .thenReturn(DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE); when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index b133a2a44fcf..af39dd44065e 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -1869,6 +1869,10 @@ public class DisplayModeDirectorTest { .thenReturn(75); when(resources.getInteger(R.integer.config_defaultRefreshRate)) .thenReturn(45); + when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + .thenReturn(65); + when(resources.getInteger(R.integer.config_defaultRefreshRateInZone)) + .thenReturn(85); when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) .thenReturn(new int[]{5}); when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) @@ -1888,6 +1892,8 @@ public class DisplayModeDirectorTest { assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 45, 0.0); assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 75, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 85); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{250}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1899,17 +1905,21 @@ public class DisplayModeDirectorTest { // Notify that the default display is updated, such that DisplayDeviceConfig has new values DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class); - when(displayDeviceConfig.getDefaultLowRefreshRate()).thenReturn(50); - when(displayDeviceConfig.getDefaultHighRefreshRate()).thenReturn(55); + when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50); + when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55); + when(displayDeviceConfig.getDefaultRefreshRate()).thenReturn(60); + when(displayDeviceConfig.getDefaultPeakRefreshRate()).thenReturn(65); when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25}); when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30}); when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210}); when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100}); director.defaultDisplayDeviceUpdated(displayDeviceConfig); - assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0); - assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 55, + assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0); + assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 65, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 55); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 50); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{210}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1922,6 +1932,8 @@ public class DisplayModeDirectorTest { // Notify that the default display is updated, such that DeviceConfig has new values FakeDeviceConfig config = mInjector.getDeviceConfig(); config.setDefaultPeakRefreshRate(60); + config.setRefreshRateInHighZone(65); + config.setRefreshRateInLowZone(70); config.setLowAmbientBrightnessThresholds(new int[]{20}); config.setLowDisplayBrightnessThresholds(new int[]{10}); config.setHighDisplayBrightnessThresholds(new int[]{255}); @@ -1929,9 +1941,11 @@ public class DisplayModeDirectorTest { director.defaultDisplayDeviceUpdated(displayDeviceConfig); - assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0); + assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0); assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 60, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 70); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{255}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1971,8 +1985,8 @@ public class DisplayModeDirectorTest { any(Handler.class)); DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class); - when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50); - when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55); + when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50); + when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55); when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25}); when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30}); when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210}); diff --git a/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java new file mode 100644 index 000000000000..24fc34849829 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java @@ -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.server.display; + +import static org.junit.Assert.assertEquals; + + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class HbmEventTest { + private long mStartTimeMillis; + private long mEndTimeMillis; + private HbmEvent mHbmEvent; + + @Before + public void setUp() { + mStartTimeMillis = 10; + mEndTimeMillis = 20; + mHbmEvent = new HbmEvent(mStartTimeMillis, mEndTimeMillis); + } + + @Test + public void getCorrectValues() { + assertEquals(mHbmEvent.getStartTimeMillis(), mStartTimeMillis); + assertEquals(mHbmEvent.getEndTimeMillis(), mEndTimeMillis); + } + + @Test + public void toStringGeneratesExpectedString() { + String actualString = mHbmEvent.toString(); + String expectedString = "HbmEvent: {startTimeMillis:" + mStartTimeMillis + + ", endTimeMillis: " + mEndTimeMillis + "}, total: " + + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]"; + assertEquals(actualString, expectedString); + } +} diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java index 53fa3e2db376..da2e1be00769 100644 --- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java @@ -27,9 +27,7 @@ import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIG import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; import static com.android.server.display.AutomaticBrightnessController .AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE; - import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; - import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID; import static org.junit.Assert.assertEquals; @@ -102,6 +100,7 @@ public class HighBrightnessModeControllerTest { private Binder mDisplayToken; private String mDisplayUniqueId; private Context mContextSpy; + private HighBrightnessModeMetadata mHighBrightnessModeMetadata; @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @@ -124,6 +123,7 @@ public class HighBrightnessModeControllerTest { mTestLooper = new TestLooper(mClock::now); mDisplayToken = null; mDisplayUniqueId = "unique_id"; + mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContextSpy); when(mContextSpy.getContentResolver()).thenReturn(resolver); @@ -140,7 +140,8 @@ public class HighBrightnessModeControllerTest { initHandler(null); final HighBrightnessModeController hbmc = new HighBrightnessModeController( mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, - mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy); + mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, + null, mContextSpy); assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f); } @@ -150,7 +151,8 @@ public class HighBrightnessModeControllerTest { initHandler(null); final HighBrightnessModeController hbmc = new HighBrightnessModeController( mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, - mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy); + mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, + null, mContextSpy); hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED); hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); @@ -705,9 +707,12 @@ public class HighBrightnessModeControllerTest { // Creates instance with standard initialization values. private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) { initHandler(clock); + if (mHighBrightnessModeMetadata == null) { + mHighBrightnessModeMetadata = new HighBrightnessModeMetadata(); + } return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, - DEFAULT_HBM_DATA, null, () -> {}, mContextSpy); + DEFAULT_HBM_DATA, null, () -> {}, mHighBrightnessModeMetadata, mContextSpy); } private void initHandler(OffsettableClock clock) { diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java new file mode 100644 index 000000000000..ede54e096ad0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.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.server.display; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class HighBrightnessModeMetadataTest { + private HighBrightnessModeMetadata mHighBrightnessModeMetadata; + + private long mRunningStartTimeMillis = -1; + + @Before + public void setUp() { + mHighBrightnessModeMetadata = new HighBrightnessModeMetadata(); + } + + @Test + public void checkDefaultValues() { + assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(), + mRunningStartTimeMillis); + assertEquals(mHighBrightnessModeMetadata.getHbmEventQueue().size(), 0); + } + + @Test + public void checkSetValues() { + mRunningStartTimeMillis = 10; + mHighBrightnessModeMetadata.setRunningStartTimeMillis(mRunningStartTimeMillis); + assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(), + mRunningStartTimeMillis); + HbmEvent expectedHbmEvent = new HbmEvent(10, 20); + mHighBrightnessModeMetadata.addHbmEvent(expectedHbmEvent); + HbmEvent actualHbmEvent = mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst(); + assertEquals(expectedHbmEvent.toString(), actualHbmEvent.toString()); + } +} 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 638637d544de..6790ad9a5da1 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY_GROUP; +import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.TYPE_INTERNAL; import static android.view.Display.TYPE_VIRTUAL; @@ -173,7 +174,7 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_NonInternalTypes() { - testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL); + testDisplayDeviceAddAndRemove_NonInternal(TYPE_EXTERNAL); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY); testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL); @@ -218,7 +219,7 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_OneExternalDefault() { - DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, + DisplayDevice device = createDisplayDevice(TYPE_EXTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); // add @@ -268,7 +269,7 @@ public class LogicalDisplayMapperTest { public void testGetDisplayIdsLocked() { add(createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0)); + add(createDisplayDevice(TYPE_EXTERNAL, 600, 800, 0)); add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0)); int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID, @@ -460,7 +461,7 @@ public class LogicalDisplayMapperTest { Layout layout = new Layout(); layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true); - layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false); + layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, true); when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout); layout = new Layout(); @@ -469,6 +470,8 @@ public class LogicalDisplayMapperTest { when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout); when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4); + LogicalDisplay display1 = add(device1); assertEquals(info(display1).address, info(device1).address); assertEquals(DEFAULT_DISPLAY, id(display1)); @@ -481,8 +484,15 @@ public class LogicalDisplayMapperTest { mLogicalDisplayMapper.setDeviceStateLocked(0, false); mLooper.moveTimeForward(1000); mLooper.dispatchAll(); + // The new state is not applied until the boot is completed assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); + + mLogicalDisplayMapper.onBootCompleted(); + mLooper.moveTimeForward(1000); + mLooper.dispatchAll(); + assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); + assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked()); @@ -623,6 +633,23 @@ public class LogicalDisplayMapperTest { assertEquals(3, threeDisplaysEnabled.length); } + @Test + public void testCreateNewLogicalDisplay() { + DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1); + LogicalDisplay display1 = add(device1); + + assertTrue(display1.isEnabledLocked()); + + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2); + LogicalDisplay display2 = add(device2); + + assertFalse(display2.isEnabledLocked()); + } + ///////////////// // Helper Methods ///////////////// diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java index 96302b954e75..299f15344dfa 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; @@ -63,6 +64,7 @@ public final class ContactsQueryHelperTest { private MatrixCursor mContactsLookupCursor; private MatrixCursor mPhoneCursor; private ContactsQueryHelper mHelper; + private ContactsContentProvider contentProvider; @Before public void setUp() { @@ -73,7 +75,7 @@ public final class ContactsQueryHelperTest { mPhoneCursor = new MatrixCursor(PHONE_COLUMNS); MockContentResolver contentResolver = new MockContentResolver(); - ContactsContentProvider contentProvider = new ContactsContentProvider(); + contentProvider = new ContactsContentProvider(); contentProvider.registerCursor(Contacts.CONTENT_URI, mContactsCursor); contentProvider.registerCursor( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, mContactsLookupCursor); @@ -89,6 +91,14 @@ public final class ContactsQueryHelperTest { } @Test + public void testQueryException_returnsFalse() { + contentProvider.setThrowException(true); + + Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY); + assertFalse(mHelper.query(contactUri.toString())); + } + + @Test public void testQueryWithUri() { mContactsCursor.addRow(new Object[] { /* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1, @@ -168,10 +178,15 @@ public final class ContactsQueryHelperTest { private class ContactsContentProvider extends MockContentProvider { private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>(); + private boolean throwException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (throwException) { + throw new SQLiteException(); + } + for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) { if (uri.isPathPrefixMatch(prefixUri)) { return mUriPrefixToCursorMap.get(prefixUri); @@ -180,6 +195,10 @@ public final class ContactsQueryHelperTest { return mUriPrefixToCursorMap.get(uri); } + public void setThrowException(boolean throwException) { + this.throwException = throwException; + } + private void registerCursor(Uri uriPrefix, Cursor cursor) { mUriPrefixToCursorMap.put(uriPrefix, cursor); } diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 66c3f0730404..a27602d65118 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -500,6 +500,7 @@ public final class DataManagerTest { // The cached conversations are above the limit because every conversation has active // notifications. To uncache one of them, the notifications for that conversation need to // be dismissed. + String notificationKey = ""; for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) { String shortcutId = TEST_SHORTCUT_ID + i; ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId, @@ -507,11 +508,13 @@ public final class DataManagerTest { shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS); mDataManager.addOrUpdateConversationInfo(shortcut); when(mNotification.getShortcutId()).thenReturn(shortcutId); - sendGenericNotification(); + notificationKey = String.format("notification-key-%d", i); + sendGenericNotificationWithKey(notificationKey); } // Post another notification for the last conversation. - sendGenericNotification(); + String otherNotificationKey = "other-notification-key"; + sendGenericNotificationWithKey(otherNotificationKey); // Removing one of the two notifications does not un-cache the shortcut. listenerService.onNotificationRemoved(mGenericSbn, null, @@ -520,6 +523,7 @@ public final class DataManagerTest { anyInt(), any(), anyString(), any(), anyInt(), anyInt()); // Removing the second notification un-caches the shortcut. + when(mGenericSbn.getKey()).thenReturn(notificationKey); listenerService.onNotificationRemoved(mGenericSbn, null, NotificationListenerService.REASON_CANCEL_ALL); verify(mShortcutServiceInternal).uncacheShortcuts( @@ -687,6 +691,63 @@ public final class DataManagerTest { } @Test + public void testGetConversation_trackActiveConversations() { + mDataManager.onUserUnlocked(USER_ID_PRIMARY); + assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID)).isNull(); + ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, + buildPerson()); + shortcut.setCached(ShortcutInfo.FLAG_PINNED); + mDataManager.addOrUpdateConversationInfo(shortcut); + assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID)).isNotNull(); + + sendGenericNotification(); + sendGenericNotification(); + ConversationChannel result = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID); + assertTrue(result.hasActiveNotifications()); + + // Both generic notifications have the same notification key, so a single dismiss will + // remove both of them. + NotificationListenerService listenerService = + mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY); + listenerService.onNotificationRemoved(mGenericSbn, null, + NotificationListenerService.REASON_CANCEL); + ConversationChannel resultTwo = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID); + assertFalse(resultTwo.hasActiveNotifications()); + } + + @Test + public void testGetConversation_unsyncedShortcut() { + mDataManager.onUserUnlocked(USER_ID_PRIMARY); + ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, + buildPerson()); + shortcut.setCached(ShortcutInfo.FLAG_PINNED); + mDataManager.addOrUpdateConversationInfo(shortcut); + assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID)).isNotNull(); + assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY) + .getConversationStore() + .getConversation(TEST_SHORTCUT_ID)).isNotNull(); + + when(mShortcutServiceInternal.getShortcuts( + anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(), + anyInt(), anyInt(), anyInt(), anyInt())) + .thenReturn(Collections.emptyList()); + assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, + TEST_SHORTCUT_ID)).isNull(); + + // Conversation is removed from store as there is no matching shortcut in ShortcutManager + assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY) + .getConversationStore() + .getConversation(TEST_SHORTCUT_ID)).isNull(); + verify(mNotificationManagerInternal) + .onConversationRemoved(TEST_PKG_NAME, TEST_PKG_UID, Set.of(TEST_SHORTCUT_ID)); + } + + @Test public void testOnNotificationChannelModified() { mDataManager.onUserUnlocked(USER_ID_PRIMARY); assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY, @@ -1294,7 +1355,7 @@ public final class DataManagerTest { sendGenericNotification(); - mDataManager.getRecentConversations(USER_ID_PRIMARY); + mDataManager.getRecentConversations(USER_ID_PRIMARY); verify(mShortcutServiceInternal).getShortcuts( anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(), @@ -1665,6 +1726,12 @@ public final class DataManagerTest { // "Sends" a notification to a non-customized notification channel - the notification channel // is something generic like "messages" and the notification has a shortcut id private void sendGenericNotification() { + sendGenericNotificationWithKey(GENERIC_KEY); + } + + // "Sends" a notification to a non-customized notification channel with the specified key. + private void sendGenericNotificationWithKey(String key) { + when(mGenericSbn.getKey()).thenReturn(key); when(mNotification.getChannelId()).thenReturn(PARENT_NOTIFICATION_CHANNEL_ID); doAnswer(invocationOnMock -> { NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking) @@ -1678,9 +1745,9 @@ public final class DataManagerTest { mParentNotificationChannel.getImportance(), null, null, mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null, - false, false, false, null, 0, false); + false, false, false, null, 0, false, 0); return true; - }).when(mRankingMap).getRanking(eq(GENERIC_KEY), + }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); NotificationListenerService listenerService = mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY); @@ -1704,7 +1771,7 @@ public final class DataManagerTest { mNotificationChannel.getImportance(), null, null, mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false, - false, false, null, 0, false); + false, false, null, 0, false, 0); return true; }).when(mRankingMap).getRanking(eq(CUSTOM_KEY), any(NotificationListenerService.Ranking.class)); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index 12cd834d1d66..8a99c2cdcc6f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -193,7 +193,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { tweak.isConversation(), tweak.getConversationShortcutInfo(), tweak.getRankingAdjustment(), - tweak.isBubble() + tweak.isBubble(), + tweak.getProposedImportance() ); assertNotEquals(nru, nru2); } @@ -274,7 +275,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { isConversation(i), getShortcutInfo(i), getRankingAdjustment(i), - isBubble(i) + isBubble(i), + getProposedImportance(i) ); rankings[i] = ranking; } @@ -402,6 +404,10 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { return index % 3 - 1; } + private int getProposedImportance(int index) { + return index % 5 - 1; + } + private boolean isBubble(int index) { return index % 4 == 0; } @@ -443,6 +449,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { assertEquals(comment, a.getConversationShortcutInfo().getId(), b.getConversationShortcutInfo().getId()); assertActionsEqual(a.getSmartActions(), b.getSmartActions()); + assertEquals(a.getProposedImportance(), b.getProposedImportance()); } private void detailedAssertEquals(RankingMap a, RankingMap b) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java new file mode 100644 index 000000000000..87e86cb00f56 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.UserHandle; +import android.service.notification.Adjustment; +import android.service.notification.SnoozeCriterion; +import android.service.notification.StatusBarNotification; + +import com.android.server.UiServiceTestCase; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Objects; + +public class NotificationRecordExtractorDataTest extends UiServiceTestCase { + + @Test + public void testHasDiffs_noDiffs() { + NotificationRecord r = generateRecord(); + + NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData( + 1, + r.getPackageVisibilityOverride(), + r.canShowBadge(), + r.canBubble(), + r.getNotification().isBubbleNotification(), + r.getChannel(), + r.getGroupKey(), + r.getPeopleOverride(), + r.getSnoozeCriteria(), + r.getUserSentiment(), + r.getSuppressedVisualEffects(), + r.getSystemGeneratedSmartActions(), + r.getSmartReplies(), + r.getImportance(), + r.getRankingScore(), + r.isConversation(), + r.getProposedImportance()); + + assertFalse(extractorData.hasDiffForRankingLocked(r, 1)); + assertFalse(extractorData.hasDiffForLoggingLocked(r, 1)); + } + + @Test + public void testHasDiffs_proposedImportanceChange() { + NotificationRecord r = generateRecord(); + + NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData( + 1, + r.getPackageVisibilityOverride(), + r.canShowBadge(), + r.canBubble(), + r.getNotification().isBubbleNotification(), + r.getChannel(), + r.getGroupKey(), + r.getPeopleOverride(), + r.getSnoozeCriteria(), + r.getUserSentiment(), + r.getSuppressedVisualEffects(), + r.getSystemGeneratedSmartActions(), + r.getSmartReplies(), + r.getImportance(), + r.getRankingScore(), + r.isConversation(), + r.getProposedImportance()); + + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH); + Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0); + r.addAdjustment(adjustment); + r.applyAdjustments(); + + assertTrue(extractorData.hasDiffForRankingLocked(r, 1)); + assertTrue(extractorData.hasDiffForLoggingLocked(r, 1)); + } + + private NotificationRecord generateRecord() { + NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); + final Notification.Builder builder = new Notification.Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon); + Notification n = builder.build(); + StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0, + 0, n, UserHandle.ALL, null, System.currentTimeMillis()); + return new NotificationRecord(getContext(), sbn, channel); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 5468220d9564..14b004827ece 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -19,6 +19,7 @@ import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -755,6 +756,24 @@ public class NotificationRecordTest extends UiServiceTestCase { } @Test + public void testProposedImportance() { + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + + assertEquals(IMPORTANCE_UNSPECIFIED, record.getProposedImportance()); + + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_DEFAULT); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); + + record.applyAdjustments(); + + assertEquals(IMPORTANCE_DEFAULT, record.getProposedImportance()); + } + + @Test public void testAppImportance_returnsCorrectly() { StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index d46530c27690..d824feecaa95 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -1703,6 +1703,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); channel.setShowBadge(true); channel.setAllowBubbles(false); + channel.setImportantConversation(true); int lockMask = 0; for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; @@ -1718,6 +1719,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights()); assertFalse(savedChannel.canBypassDnd()); assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); + assertFalse(channel.isImportantConversation()); assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); assertEquals(channel.canBubble(), savedChannel.canBubble()); @@ -4396,7 +4398,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); channel2.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false); List<ConversationChannelWrapper> convos = mHelper.getConversations(IntArray.wrap(new int[] {0}), false); @@ -4473,7 +4475,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); channel2.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false); List<ConversationChannelWrapper> convos = mHelper.getConversations(IntArray.wrap(new int[] {0}), false); @@ -4501,13 +4503,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); channel.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false); NotificationChannel diffConvo = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); diffConvo.setConversationId(p.getId(), "different convo"); diffConvo.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, false, false); NotificationChannel channel2 = new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); @@ -4534,7 +4536,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); channel.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false); mHelper.permanentlyDeleteNotificationChannel(PKG_O, UID_O, "messages"); @@ -4935,7 +4937,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { "conversation", IMPORTANCE_DEFAULT); friend.setConversationId(parent.getId(), "friend"); friend.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, false, false); ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackageChannelPreferencesStats(events); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index eed32d7d815c..bb20244aee91 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -249,6 +249,19 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { } /** + * Ensures it updates recent tasks order when the last resumed activity changed. + */ + @Test + public void testUpdateRecentTasksForTopResumed() { + spyOn(mSupervisor.mRecentTasks); + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = activity.getTask(); + + mAtm.setLastResumedActivityUncheckLocked(activity, "test"); + verify(mSupervisor.mRecentTasks).add(eq(task)); + } + + /** * Ensures that a trusted display can launch arbitrary activity and an untrusted display can't. */ @Test 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 e30e5dbcaf46..74ea7d7687ed 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -40,7 +40,7 @@ import android.hardware.HardwareBuffer; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.view.WindowManager; -import android.window.BackEvent; +import android.window.BackMotionEvent; import android.window.BackNavigationInfo; import android.window.IOnBackInvokedCallback; import android.window.OnBackInvokedCallback; @@ -242,11 +242,11 @@ public class BackNavigationControllerTests extends WindowTestsBase { private IOnBackInvokedCallback createOnBackInvokedCallback() { return new IOnBackInvokedCallback.Stub() { @Override - public void onBackStarted(BackEvent backEvent) { + public void onBackStarted(BackMotionEvent backEvent) { } @Override - public void onBackProgressed(BackEvent backEvent) { + public void onBackProgressed(BackMotionEvent backEvent) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index 8bb79e3f7ddc..45b30b204801 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -155,6 +155,18 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testTreatmentDisabledPerApp_noForceRotationOrRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent); @@ -327,7 +339,21 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception { + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh() + throws Exception { when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -362,6 +388,19 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); } + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat()) + .thenReturn(true); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + private void configureActivity(@ScreenOrientation int activityOrientation) { configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 6d778afee88c..5e087f06b36b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -16,8 +16,14 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -74,6 +80,8 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mController = new LetterboxUiController(mWm, mActivity); } + // shouldIgnoreRequestedOrientation + @Test @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() { @@ -134,7 +142,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() { prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); doReturn(false).when(mLetterboxConfiguration) @@ -143,6 +151,163 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); } + // shouldRefreshActivityForCameraCompat + + @Test + public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityForCameraCompat()); + } + + // shouldRefreshActivityViaPauseForCameraCompat + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + // shouldForceRotateForCameraCompat + + @Test + public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldForceRotateForCameraCompat()); + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index adf694c2a88d..db6ac0b432b3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -30,6 +30,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.os.Process.NOBODY_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -1220,20 +1221,35 @@ public class RecentTasksTest extends WindowTestsBase { @Test public void testCreateRecentTaskInfo_detachedTask() { - final Task task = createTaskBuilder(".Task").setCreateActivity(true).build(); + final Task task = createTaskBuilder(".Task").build(); + final ComponentName componentName = getUniqueComponentName(); + new ActivityBuilder(mSupervisor.mService) + .setTask(task) + .setUid(NOBODY_UID) + .setComponent(componentName) + .build(); final TaskDisplayArea tda = task.getDisplayArea(); assertTrue(task.isAttached()); assertTrue(task.supportsMultiWindow()); - RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true); + RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + false /* getTasksAllowed */); + + assertFalse(info.topActivity.equals(componentName)); + assertFalse(info.topActivityInfo.packageName.equals(componentName.getPackageName())); + assertFalse(info.baseActivity.equals(componentName)); + // The task can be put in split screen even if it is not attached now. task.removeImmediately(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); @@ -1242,7 +1258,8 @@ public class RecentTasksTest extends WindowTestsBase { doReturn(false).when(tda).supportsNonResizableMultiWindow(); doReturn(false).when(task).isResizeable(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertFalse(info.supportsMultiWindow); @@ -1250,7 +1267,8 @@ public class RecentTasksTest extends WindowTestsBase { // the device supports it. doReturn(true).when(tda).supportsNonResizableMultiWindow(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index a8e91980014e..995932c46201 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -197,27 +197,6 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testNotApplyStrategyToTranslucentActivitiesWithDifferentUid() { - mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); - setUpDisplaySizeWithApp(2000, 1000); - prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); - mActivity.info.setMinAspectRatio(1.2f); - mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - // Translucent Activity - final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) - .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) - .setMinAspectRatio(1.1f) - .setMaxAspectRatio(3f) - .build(); - doReturn(false).when(translucentActivity).fillsParent(); - mTask.addChild(translucentActivity); - // We check bounds - final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); - final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds(); - assertNotEquals(opaqueBounds, translucentRequestedBounds); - } - - @Test public void testApplyStrategyToMultipleTranslucentActivities() { mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); setUpDisplaySizeWithApp(2000, 1000); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 2420efc63b80..8244f9419b80 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -796,6 +796,72 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testApplyTransaction_createTaskFragment_overrideBounds() { + final Task task = createTask(mDisplayContent); + final ActivityRecord activityAtBottom = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activityAtBottom.info.applicationInfo.uid = uid; + activityAtBottom.getTask().effectiveUid = uid; + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .createActivityCount(1) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final IBinder fragmentToken1 = new Binder(); + final Rect bounds = new Rect(100, 100, 500, 1000); + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken1, activityAtBottom.token) + .setPairedActivityToken(activityAtBottom.token) + .setInitialBounds(bounds) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // Successfully created a TaskFragment. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + fragmentToken1); + assertNotNull(taskFragment); + // The relative embedded bounds is updated to the initial requested bounds. + assertEquals(bounds, taskFragment.getRelativeEmbeddedBounds()); + } + + @Test + public void testApplyTransaction_createTaskFragment_withPairedActivityToken() { + final Task task = createTask(mDisplayContent); + final ActivityRecord activityAtBottom = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activityAtBottom.info.applicationInfo.uid = uid; + activityAtBottom.getTask().effectiveUid = uid; + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .createActivityCount(1) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final IBinder fragmentToken1 = new Binder(); + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken1, activityAtBottom.token) + .setPairedActivityToken(activityAtBottom.token) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // Successfully created a TaskFragment. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + fragmentToken1); + assertNotNull(taskFragment); + // The new TaskFragment should be positioned right above the paired activity. + assertEquals(task.mChildren.indexOf(activityAtBottom) + 1, + task.mChildren.indexOf(taskFragment)); + // The top TaskFragment should remain on top. + assertEquals(task.mChildren.indexOf(taskFragment) + 1, + task.mChildren.indexOf(mTaskFragment)); + } + + @Test public void testApplyTransaction_enforceHierarchyChange_reparentChildren() { doReturn(true).when(mTaskFragment).isAttached(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 06a79f47de55..1407cdd8600c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -391,7 +391,7 @@ public class WallpaperControllerTests extends WindowTestsBase { dc.updateOrientation(); dc.sendNewConfiguration(); spyOn(wallpaperWindow); - doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds(); + doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 0568f2acf366..fd3776f828e5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -42,6 +42,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; @@ -544,7 +545,12 @@ public class WindowStateTests extends WindowTestsBase { win.applyWithNextDraw(t -> handledT[0] = t); assertTrue(win.useBLASTSync()); final SurfaceControl.Transaction drawT = new StubTransaction(); + final SurfaceControl.Transaction currT = win.getSyncTransaction(); + clearInvocations(currT); + win.mWinAnimator.mLastHidden = true; assertTrue(win.finishDrawing(drawT, Integer.MAX_VALUE)); + // The draw transaction should be merged to current transaction even if the state is hidden. + verify(currT).merge(eq(drawT)); assertEquals(drawT, handledT[0]); assertFalse(win.useBLASTSync()); @@ -969,6 +975,19 @@ public class WindowStateTests extends WindowTestsBase { assertFalse(sameTokenWindow.needsRelativeLayeringToIme()); } + @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) + @Test + public void testNeedsRelativeLayeringToIme_systemDialog() { + WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, + "SystemDialog", true); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + makeWindowVisible(mImeWindow); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + assertTrue(systemDialogWindow.needsRelativeLayeringToIme()); + } + @Test public void testSetFreezeInsetsState() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index 77fca451547d..7959d82ae22f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -31,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; @@ -543,4 +545,28 @@ public class ZOrderingTests extends WindowTestsBase { assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(), mDisplayContent.getImeContainer().getSurfaceControl()); } + + @Test + public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() { + // Simulate the app window is in multi windowing mode and being IME target + mAppWindow.getConfiguration().windowConfiguration.setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + makeWindowVisible(mImeWindow); + + // Create a popupWindow + final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, "SystemDialog", true); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + spyOn(systemDialogWindow); + + mDisplayContent.assignChildLayers(mTransaction); + + // Verify the surface layer of the popupWindow should higher than IME + verify(systemDialogWindow).needsRelativeLayeringToIme(); + assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue(); + assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(), + mDisplayContent.getImeContainer().getSurfaceControl()); + } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index d0a536bc6d77..04b08d471dde 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -55,6 +55,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1152,6 +1153,11 @@ final class HotwordDetectionConnection { Slog.w(TAG, "Failed to report onError status: " + e); } } + // Can improve to log exit reason if needed + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH, + mVoiceInteractionServiceUid); } @Override diff --git a/telephony/java/android/telephony/NetworkScanRequest.java b/telephony/java/android/telephony/NetworkScanRequest.java index 326f4171de3b..65c2146b4819 100644 --- a/telephony/java/android/telephony/NetworkScanRequest.java +++ b/telephony/java/android/telephony/NetworkScanRequest.java @@ -26,7 +26,7 @@ import java.util.ArrayList; import java.util.Arrays; /** - * Defines a request to peform a network scan. + * Defines a request to perform a network scan. * * This class defines whether the network scan will be performed only once or periodically until * cancelled, when the scan is performed periodically, the time interval is not controlled by the |