diff options
351 files changed, 8150 insertions, 2640 deletions
diff --git a/core/java/android/accounts/ChooseTypeAndAccountActivity.java b/core/java/android/accounts/ChooseTypeAndAccountActivity.java index f623295dee3e..6e02390bb711 100644 --- a/core/java/android/accounts/ChooseTypeAndAccountActivity.java +++ b/core/java/android/accounts/ChooseTypeAndAccountActivity.java @@ -402,7 +402,7 @@ public class ChooseTypeAndAccountActivity extends Activity mExistingAccounts = AccountManager.get(this).getAccountsForPackage(mCallingPackage, mCallingUid); intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); - startActivityForResult(intent, REQUEST_ADD_ACCOUNT); + startActivityForResult(new Intent(intent), REQUEST_ADD_ACCOUNT); return; } } catch (OperationCanceledException e) { diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 085bfca158c1..5c1da1112e2a 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -938,7 +938,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing public void onPointerDown(long requestId, int sensorId, int x, int y, float minor, float major) { if (mService == null) { - Slog.w(TAG, "onFingerDown: no fingerprint service"); + Slog.w(TAG, "onPointerDown: no fingerprint service"); return; } @@ -955,7 +955,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing @RequiresPermission(USE_BIOMETRIC_INTERNAL) public void onPointerUp(long requestId, int sensorId) { if (mService == null) { - Slog.w(TAG, "onFingerDown: no fingerprint service"); + Slog.w(TAG, "onPointerUp: no fingerprint service"); return; } @@ -967,6 +967,58 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** + * TODO(b/218388821): The parameter list should be replaced with PointerContext. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void onPointerDown( + long requestId, + int sensorId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { + if (mService == null) { + Slog.w(TAG, "onPointerDown: no fingerprint service"); + return; + } + + // TODO(b/218388821): Propagate all the parameters to FingerprintService. + Slog.e(TAG, "onPointerDown: not implemented!"); + } + + /** + * TODO(b/218388821): The parameter list should be replaced with PointerContext. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void onPointerUp( + long requestId, + int sensorId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { + if (mService == null) { + Slog.w(TAG, "onPointerUp: no fingerprint service"); + return; + } + + // TODO(b/218388821): Propagate all the parameters to FingerprintService. + Slog.e(TAG, "onPointerUp: not implemented!"); + } + + /** * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java index c54d9b604ba2..3e7c67e72031 100644 --- a/core/java/android/util/RotationUtils.java +++ b/core/java/android/util/RotationUtils.java @@ -25,6 +25,7 @@ import android.annotation.Dimension; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.view.Surface.Rotation; import android.view.SurfaceControl; @@ -193,6 +194,29 @@ public class RotationUtils { } /** + * Same as {@link #rotatePoint}, but for float coordinates. + */ + public static void rotatePointF(PointF inOutPoint, @Rotation int rotation, + float parentW, float parentH) { + float origX = inOutPoint.x; + switch (rotation) { + case ROTATION_0: + return; + case ROTATION_90: + inOutPoint.x = inOutPoint.y; + inOutPoint.y = parentW - origX; + return; + case ROTATION_180: + inOutPoint.x = parentW - inOutPoint.x; + inOutPoint.y = parentH - inOutPoint.y; + return; + case ROTATION_270: + inOutPoint.x = parentH - inOutPoint.y; + inOutPoint.y = origX; + } + } + + /** * Sets a matrix such that given a rotation, it transforms physical display * coordinates to that rotation's logical coordinates. * diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index bb26c46142d2..1f095ac5f40d 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -727,9 +727,8 @@ interface IWindowManager * If invoked through a package other than a launcher app, returns an empty list. * * @param displayId the id of the logical display - * @param packageName the name of the calling package */ - List<DisplayInfo> getPossibleDisplayInfo(int displayId, String packageName); + List<DisplayInfo> getPossibleDisplayInfo(int displayId); /** * Called to show global actions. diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 20cdad42b4cc..baf99254f290 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -357,7 +357,7 @@ public final class WindowManagerImpl implements WindowManager { List<DisplayInfo> possibleDisplayInfos; try { possibleDisplayInfos = WindowManagerGlobal.getWindowManagerService() - .getPossibleDisplayInfo(displayId, mContext.getPackageName()); + .getPossibleDisplayInfo(displayId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 0956a71bd92d..666f316f7c1d 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -414,6 +414,53 @@ public final class TransitionInfo implements Parcelable { return false; } + /** + * Releases temporary-for-animation surfaces referenced by this to potentially free up memory. + * This includes root-leash and snapshots. + */ + public void releaseAnimSurfaces() { + for (int i = mChanges.size() - 1; i >= 0; --i) { + final Change c = mChanges.get(i); + if (c.mSnapshot != null) { + c.mSnapshot.release(); + c.mSnapshot = null; + } + } + if (mRootLeash != null) { + mRootLeash.release(); + } + } + + /** + * Releases ALL the surfaces referenced by this to potentially free up memory. Do NOT use this + * if the surface-controls get stored and used elsewhere in the process. To just release + * temporary-for-animation surfaces, use {@link #releaseAnimSurfaces}. + */ + public void releaseAllSurfaces() { + releaseAnimSurfaces(); + for (int i = mChanges.size() - 1; i >= 0; --i) { + mChanges.get(i).getLeash().release(); + } + } + + /** + * Makes a copy of this as if it were parcel'd and unparcel'd. This implies that surfacecontrol + * refcounts are incremented which allows the "remote" receiver to release them without breaking + * the caller's references. Use this only if you need to "send" this to a local function which + * assumes it is being called from a remote caller. + */ + public TransitionInfo localRemoteCopy() { + final TransitionInfo out = new TransitionInfo(mType, mFlags); + for (int i = 0; i < mChanges.size(); ++i) { + out.mChanges.add(mChanges.get(i).localRemoteCopy()); + } + out.mRootLeash = mRootLeash != null ? new SurfaceControl(mRootLeash, "localRemote") : null; + // Doesn't have any native stuff, so no need for actual copy + out.mOptions = mOptions; + out.mRootOffset.set(mRootOffset); + return out; + } + /** Represents the change a WindowContainer undergoes during a transition */ public static final class Change implements Parcelable { private final WindowContainerToken mContainer; @@ -466,6 +513,27 @@ public final class TransitionInfo implements Parcelable { mSnapshotLuma = in.readFloat(); } + private Change localRemoteCopy() { + final Change out = new Change(mContainer, new SurfaceControl(mLeash, "localRemote")); + out.mParent = mParent; + out.mLastParent = mLastParent; + out.mMode = mMode; + out.mFlags = mFlags; + out.mStartAbsBounds.set(mStartAbsBounds); + out.mEndAbsBounds.set(mEndAbsBounds); + out.mEndRelOffset.set(mEndRelOffset); + out.mTaskInfo = mTaskInfo; + out.mAllowEnterPip = mAllowEnterPip; + out.mStartRotation = mStartRotation; + out.mEndRotation = mEndRotation; + out.mEndFixedRotation = mEndFixedRotation; + out.mRotationAnimation = mRotationAnimation; + out.mBackgroundColor = mBackgroundColor; + out.mSnapshot = mSnapshot != null ? new SurfaceControl(mSnapshot, "localRemote") : null; + out.mSnapshotLuma = mSnapshotLuma; + return out; + } + /** Sets the parent of this change's container. The parent must be a participant or null. */ public void setParent(@Nullable WindowContainerToken parent) { mParent = parent; diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index fcbe2b8b62b3..d956e9ac2f9c 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -91,7 +91,7 @@ <string name="notification_channel_network_alert" msgid="4788053066033851841">"Alertas"</string> <string name="notification_channel_call_forward" msgid="8230490317314272406">"Desvío de llamadas"</string> <string name="notification_channel_emergency_callback" msgid="54074839059123159">"Modo de devolución de llamada de emergencia"</string> - <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"Estado de los datos móviles"</string> + <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"Estado de datos móviles"</string> <string name="notification_channel_sms" msgid="1243384981025535724">"Mensajes SMS"</string> <string name="notification_channel_voice_mail" msgid="8457433203106654172">"Mensajes de voz"</string> <string name="notification_channel_wfc" msgid="9048240466765169038">"Llamada por Wi-Fi"</string> diff --git a/core/res/res/values-gl/strings.xml b/core/res/res/values-gl/strings.xml index 9b6818660662..57dcc9062099 100644 --- a/core/res/res/values-gl/strings.xml +++ b/core/res/res/values-gl/strings.xml @@ -1949,13 +1949,13 @@ <string name="app_streaming_blocked_title_for_settings_dialog" product="tv" msgid="196994247017450357">"A configuración de Android TV non está dispoñible"</string> <string name="app_streaming_blocked_title_for_settings_dialog" product="tablet" msgid="8222710146267948647">"A configuración da tableta non está dispoñible"</string> <string name="app_streaming_blocked_title_for_settings_dialog" product="default" msgid="6895719984375299791">"A configuración do teléfono non está dispoñible"</string> - <string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"Nestes momentos, non podes acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o dispositivo con Android TV."</string> + <string name="app_streaming_blocked_message" product="tv" msgid="4003011766528814377">"Nestes momentos, non podes acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o dispositivo Android TV."</string> <string name="app_streaming_blocked_message" product="tablet" msgid="4242053045964946062">"Nestes momentos, non podes acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde a tableta."</string> <string name="app_streaming_blocked_message" product="default" msgid="6159168735030739398">"Nestes momentos, non podes acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o teléfono."</string> - <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Esta aplicación solicita seguranza adicional. Proba a facelo desde o dispositivo con Android TV."</string> + <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv" msgid="3470977315395784567">"Esta aplicación solicita seguranza adicional. Proba a facelo desde o dispositivo Android TV."</string> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet" msgid="698460091901465092">"Esta aplicación solicita seguranza adicional. Proba a facelo desde a tableta."</string> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default" msgid="8552691971910603907">"Esta aplicación solicita seguranza adicional. Proba a facelo desde o teléfono."</string> - <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"Non se puido acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o dispositivo con Android TV."</string> + <string name="app_streaming_blocked_message_for_settings_dialog" product="tv" msgid="820334666354451145">"Non se puido acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o dispositivo Android TV."</string> <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet" msgid="3286849551133045896">"Non se puido acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde a tableta."</string> <string name="app_streaming_blocked_message_for_settings_dialog" product="default" msgid="6264287556598916295">"Non se puido acceder a este contido desde o teu dispositivo (<xliff:g id="DEVICE">%1$s</xliff:g>). Proba a facelo desde o teléfono."</string> <string name="deprecated_target_sdk_message" msgid="5203207875657579953">"Esta aplicación deseñouse para unha versión anterior de Android e quizais non funcione correctamente. Proba a buscar actualizacións ou contacta co programador."</string> diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index 251a53a85765..6b80aa3355a4 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -260,7 +260,7 @@ <string name="global_action_toggle_silent_mode" msgid="8464352592860372188">"Stille modus"</string> <string name="global_action_silent_mode_on_status" msgid="2371892537738632013">"Geluid is UIT"</string> <string name="global_action_silent_mode_off_status" msgid="6608006545950920042">"Geluid is AAN"</string> - <string name="global_actions_toggle_airplane_mode" msgid="6911684460146916206">"Vliegtuigmodus"</string> + <string name="global_actions_toggle_airplane_mode" msgid="6911684460146916206">"Vliegtuigmodus"</string> <string name="global_actions_airplane_mode_on_status" msgid="5508025516695361936">"Vliegtuigmodus is AAN"</string> <string name="global_actions_airplane_mode_off_status" msgid="8522219771500505475">"Vliegtuigmodus is UIT"</string> <string name="global_action_settings" msgid="4671878836947494217">"Instellingen"</string> @@ -990,7 +990,7 @@ <string name="keyguard_accessibility_user_selector" msgid="1466067610235696600">"Gebruikersselectie"</string> <string name="keyguard_accessibility_status" msgid="6792745049712397237">"Status"</string> <string name="keyguard_accessibility_camera" msgid="7862557559464986528">"Camera"</string> - <string name="keygaurd_accessibility_media_controls" msgid="2267379779900620614">"Mediabediening"</string> + <string name="keygaurd_accessibility_media_controls" msgid="2267379779900620614">"Mediabediening"</string> <string name="keyguard_accessibility_widget_reorder_start" msgid="7066213328912939191">"Opnieuw indelen van widget gestart."</string> <string name="keyguard_accessibility_widget_reorder_end" msgid="1083806817600593490">"Opnieuw indelen van widget beëindigd."</string> <string name="keyguard_accessibility_widget_deleted" msgid="1509738950119878705">"Widget <xliff:g id="WIDGET_INDEX">%1$s</xliff:g> verwijderd."</string> @@ -1482,7 +1482,7 @@ <string name="forward_intent_to_work" msgid="3620262405636021151">"U gebruikt deze app in je werkprofiel"</string> <string name="input_method_binding_label" msgid="1166731601721983656">"Invoermethode"</string> <string name="sync_binding_label" msgid="469249309424662147">"Synchroniseren"</string> - <string name="accessibility_binding_label" msgid="1974602776545801715">"Toegankelijkheid"</string> + <string name="accessibility_binding_label" msgid="1974602776545801715">"Toegankelijkheid"</string> <string name="wallpaper_binding_label" msgid="1197440498000786738">"Achtergrond"</string> <string name="chooser_wallpaper" msgid="3082405680079923708">"Achtergrond wijzigen"</string> <string name="notification_listener_binding_label" msgid="2702165274471499713">"Listener voor meldingen"</string> @@ -1986,7 +1986,7 @@ <string name="app_category_news" msgid="1172762719574964544">"Nieuws en tijdschriften"</string> <string name="app_category_maps" msgid="6395725487922533156">"Maps en navigatie"</string> <string name="app_category_productivity" msgid="1844422703029557883">"Productiviteit"</string> - <string name="app_category_accessibility" msgid="6643521607848547683">"Toegankelijkheid"</string> + <string name="app_category_accessibility" msgid="6643521607848547683">"Toegankelijkheid"</string> <string name="device_storage_monitor_notification_channel" msgid="5164244565844470758">"Apparaatopslag"</string> <string name="adb_debugging_notification_channel_tv" msgid="4764046459631031496">"USB-foutopsporing"</string> <string name="time_picker_hour_label" msgid="4208590187662336864">"uur"</string> diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml index 26bfeb4acec2..dfb64e2c0621 100644 --- a/core/res/res/values-or/strings.xml +++ b/core/res/res/values-or/strings.xml @@ -90,7 +90,7 @@ <string name="notification_channel_network_alert" msgid="4788053066033851841">"ଆଲର୍ଟ"</string> <string name="notification_channel_call_forward" msgid="8230490317314272406">"କଲ୍ ଫରୱାର୍ଡିଂ"</string> <string name="notification_channel_emergency_callback" msgid="54074839059123159">"ଜରୁରୀକାଳୀନ କଲବ୍ୟାକ୍ ମୋଡ୍"</string> - <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"ମୋବାଇଲ୍ ଡାଟା ଷ୍ଟାଟସ୍"</string> + <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"ମୋବାଇଲ ଡାଟା ଷ୍ଟାଟସ"</string> <string name="notification_channel_sms" msgid="1243384981025535724">"SMS ମେସେଜ୍"</string> <string name="notification_channel_voice_mail" msgid="8457433203106654172">"ଭଏସମେଲ୍ ମେସେଜ୍"</string> <string name="notification_channel_wfc" msgid="9048240466765169038">"ୱାଇ-ଫାଇ କଲିଙ୍ଗ"</string> @@ -799,30 +799,30 @@ <item msgid="8150904584178569699">"ୱାର୍କ ଫ୍ୟାକ୍ସ"</item> <item msgid="4537253139152229577">"ହୋମ ଫାକ୍ସ"</item> <item msgid="6751245029698664340">"ପେଜର୍"</item> - <item msgid="1692790665884224905">"ଅନ୍ୟାନ୍ୟ"</item> + <item msgid="1692790665884224905">"ଅନ୍ୟ"</item> <item msgid="6216981255272016212">"କଷ୍ଟମ୍"</item> </string-array> <string-array name="emailAddressTypes"> <item msgid="7786349763648997741">"ହୋମ"</item> <item msgid="435564470865989199">"ୱାର୍କ"</item> - <item msgid="4199433197875490373">"ଅନ୍ୟାନ୍ୟ"</item> + <item msgid="4199433197875490373">"ଅନ୍ୟ"</item> <item msgid="3233938986670468328">"କଷ୍ଟମ୍"</item> </string-array> <string-array name="postalAddressTypes"> <item msgid="3861463339764243038">"ହୋମ"</item> <item msgid="5472578890164979109">"ୱାର୍କ"</item> - <item msgid="5718921296646594739">"ଅନ୍ୟାନ୍ୟ"</item> + <item msgid="5718921296646594739">"ଅନ୍ୟ"</item> <item msgid="5523122236731783179">"କଷ୍ଟମ୍"</item> </string-array> <string-array name="imAddressTypes"> <item msgid="588088543406993772">"ହୋମ"</item> <item msgid="5503060422020476757">"ୱାର୍କ"</item> - <item msgid="2530391194653760297">"ଅନ୍ୟାନ୍ୟ"</item> + <item msgid="2530391194653760297">"ଅନ୍ୟ"</item> <item msgid="7640927178025203330">"କଷ୍ଟମ୍"</item> </string-array> <string-array name="organizationTypes"> <item msgid="6144047813304847762">"ୱାର୍କ"</item> - <item msgid="7402720230065674193">"ଅନ୍ୟାନ୍ୟ"</item> + <item msgid="7402720230065674193">"ଅନ୍ୟ"</item> <item msgid="808230403067569648">"କଷ୍ଟମ୍"</item> </string-array> <string-array name="imProtocols"> @@ -842,7 +842,7 @@ <string name="phoneTypeFaxWork" msgid="6757519896109439123">"ୱାର୍କ ଫାକ୍ସ"</string> <string name="phoneTypeFaxHome" msgid="6678559953115904345">"ହୋମ ଫାକ୍ସ"</string> <string name="phoneTypePager" msgid="576402072263522767">"ପେଜର୍"</string> - <string name="phoneTypeOther" msgid="6918196243648754715">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="phoneTypeOther" msgid="6918196243648754715">"ଅନ୍ୟ"</string> <string name="phoneTypeCallback" msgid="3455781500844157767">"କଲବ୍ୟାକ୍"</string> <string name="phoneTypeCar" msgid="4604775148963129195">"କାର୍"</string> <string name="phoneTypeCompanyMain" msgid="4482773154536455441">"କମ୍ପାନୀର ମୁଖ୍ୟ"</string> @@ -863,16 +863,16 @@ <string name="emailTypeCustom" msgid="1809435350482181786">"କଷ୍ଟମ୍"</string> <string name="emailTypeHome" msgid="1597116303154775999">"ହୋମ"</string> <string name="emailTypeWork" msgid="2020095414401882111">"ୱାର୍କ"</string> - <string name="emailTypeOther" msgid="5131130857030897465">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="emailTypeOther" msgid="5131130857030897465">"ଅନ୍ୟ"</string> <string name="emailTypeMobile" msgid="787155077375364230">"ମୋବାଇଲ୍"</string> <string name="postalTypeCustom" msgid="5645590470242939129">"କଷ୍ଟମ୍"</string> <string name="postalTypeHome" msgid="7562272480949727912">"ହୋମ"</string> <string name="postalTypeWork" msgid="8553425424652012826">"ୱାର୍କ"</string> - <string name="postalTypeOther" msgid="7094245413678857420">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="postalTypeOther" msgid="7094245413678857420">"ଅନ୍ୟ"</string> <string name="imTypeCustom" msgid="5653384545085765570">"କଷ୍ଟମ୍"</string> <string name="imTypeHome" msgid="6996507981044278216">"ହୋମ"</string> <string name="imTypeWork" msgid="2099668940169903123">"ୱାର୍କ"</string> - <string name="imTypeOther" msgid="8068447383276219810">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="imTypeOther" msgid="8068447383276219810">"ଅନ୍ୟ"</string> <string name="imProtocolCustom" msgid="4437878287653764692">"କଷ୍ଟମ୍"</string> <string name="imProtocolAim" msgid="4050198236506604378">"AIM"</string> <string name="imProtocolMsn" msgid="2257148557766499232">"Windows Live"</string> @@ -884,7 +884,7 @@ <string name="imProtocolJabber" msgid="7919269388889582015">"Jabber"</string> <string name="imProtocolNetMeeting" msgid="4985002408136148256">"NetMeeting"</string> <string name="orgTypeWork" msgid="8684458700669564172">"ୱାର୍କ"</string> - <string name="orgTypeOther" msgid="5450675258408005553">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="orgTypeOther" msgid="5450675258408005553">"ଅନ୍ୟ"</string> <string name="orgTypeCustom" msgid="1126322047677329218">"କଷ୍ଟମ୍"</string> <string name="relationTypeCustom" msgid="282938315217441351">"କଷ୍ଟମ୍"</string> <string name="relationTypeAssistant" msgid="4057605157116589315">"Assistant"</string> @@ -904,7 +904,7 @@ <string name="sipAddressTypeCustom" msgid="6283889809842649336">"କଷ୍ଟମ୍"</string> <string name="sipAddressTypeHome" msgid="5918441930656878367">"ହୋମ"</string> <string name="sipAddressTypeWork" msgid="7873967986701216770">"ୱାର୍କ"</string> - <string name="sipAddressTypeOther" msgid="6317012577345187275">"ଅନ୍ୟାନ୍ୟ"</string> + <string name="sipAddressTypeOther" msgid="6317012577345187275">"ଅନ୍ୟ"</string> <string name="quick_contacts_not_available" msgid="1262709196045052223">"ଏହି କଣ୍ଟାକ୍ଟ ଦେଖିବାକୁ କୌଣସି ଆପ୍ଲିକେସନ ମିଳିଲା ନାହିଁ।"</string> <string name="keyguard_password_enter_pin_code" msgid="6401406801060956153">"PIN କୋଡ୍ ଟାଇପ୍ କରନ୍ତୁ"</string> <string name="keyguard_password_enter_puk_code" msgid="3112256684547584093">"PUK ଓ ନୂଆ PIN କୋଡ୍ ଟାଇପ୍ କରନ୍ତୁ"</string> diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index c2fe6ddd103a..318971dced42 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -92,7 +92,7 @@ <string name="notification_channel_network_alert" msgid="4788053066033851841">"Оповещения"</string> <string name="notification_channel_call_forward" msgid="8230490317314272406">"Переадресация вызовов"</string> <string name="notification_channel_emergency_callback" msgid="54074839059123159">"Режим экстренных обратных вызовов"</string> - <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"Состояние мобильного Интернета"</string> + <string name="notification_channel_mobile_data_status" msgid="1941911162076442474">"Состояние мобильного интернета"</string> <string name="notification_channel_sms" msgid="1243384981025535724">"SMS"</string> <string name="notification_channel_voice_mail" msgid="8457433203106654172">"Голосовые сообщения"</string> <string name="notification_channel_wfc" msgid="9048240466765169038">"Звонки по Wi-Fi"</string> diff --git a/core/tests/coretests/src/android/util/RotationUtilsTest.java b/core/tests/coretests/src/android/util/RotationUtilsTest.java index 826eb3070c7e..1b1ee4fb1a1c 100644 --- a/core/tests/coretests/src/android/util/RotationUtilsTest.java +++ b/core/tests/coretests/src/android/util/RotationUtilsTest.java @@ -18,6 +18,7 @@ package android.util; import static android.util.RotationUtils.rotateBounds; import static android.util.RotationUtils.rotatePoint; +import static android.util.RotationUtils.rotatePointF; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -25,6 +26,7 @@ import static android.view.Surface.ROTATION_90; import static org.junit.Assert.assertEquals; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -79,4 +81,26 @@ public class RotationUtilsTest { rotatePoint(testResult, ROTATION_270, parentW, parentH); assertEquals(new Point(560, 60), testResult); } + + @Test + public void testRotatePointF() { + float parentW = 1000f; + float parentH = 600f; + PointF testPt = new PointF(60f, 40f); + + PointF testResult = new PointF(testPt); + rotatePointF(testResult, ROTATION_90, parentW, parentH); + assertEquals(40f, testResult.x, .1f); + assertEquals(940f, testResult.y, .1f); + + testResult.set(testPt.x, testPt.y); + rotatePointF(testResult, ROTATION_180, parentW, parentH); + assertEquals(940f, testResult.x, .1f); + assertEquals(560f, testResult.y, .1f); + + testResult.set(testPt.x, testPt.y); + rotatePointF(testResult, ROTATION_270, parentW, parentH); + assertEquals(560f, testResult.x, .1f); + assertEquals(60f, testResult.y, .1f); + } } diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index dfce50174bda..3d50d2262bb8 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Laat los"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Program sal dalk nie met verdeelde skerm werk nie."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Program steun nie verdeelde skerm nie."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Hierdie app kan net in 1 venster oopgemaak word."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Program sal dalk nie op \'n sekondêre skerm werk nie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Program steun nie begin op sekondêre skerms nie."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skermverdeler"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 655e53f64254..a0213f42b125 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"দেখুৱাওক"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"এপ্টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপ্টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে।"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই এপ্টো কেৱল ১ খন ৱিণ্ড’ত খুলিব পাৰি।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"গৌণ ডিছপ্লেত এপে সঠিকভাৱে কাম নকৰিব পাৰে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string> <string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index d3d20587cb68..f842bfe13efc 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Güvənli məkandan çıxarın"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Tətbiq bölünmüş ekran ilə işləməyə bilər."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Tətbiq ekran bölünməsini dəstəkləmir."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu tətbiq yalnız 1 pəncərədə açıla bilər."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Tətbiq ikinci ekranda işləməyə bilər."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Tətbiq ikinci ekranda başlamağı dəstəkləmir."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcısı"</string> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index 839ecb60e395..540ae7ce6953 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Uklonite iz tajne memorije"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi sa podeljenim ekranom."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podeljeni ekran."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova aplikacija može da se otvori samo u jednom prozoru."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionisati na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdelnik podeljenog ekrana"</string> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index b227cf12a1b0..bea753837b7b 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Паказаць"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Праграма можа не працаваць у рэжыме падзеленага экрана."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Праграма не падтрымлівае функцыю дзялення экрана."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Гэту праграму можна адкрыць толькі ў адным акне."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Праграма можа не працаваць на дадатковых экранах."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Праграма не падтрымлівае запуск на дадатковых экранах."</string> <string name="accessibility_divider" msgid="703810061635792791">"Раздзяляльнік падзеленага экрана"</string> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 20f40387665e..59915e6b2a6e 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Отмяна на съхраняването"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Приложението може да не работи в режим на разделен екран."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложението не поддържа разделен екран."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Това приложение може да се отвори само в 1 прозорец."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Възможно е приложението да не работи на алтернативни дисплеи."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложението не поддържа използването на алтернативни дисплеи."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделител в режима за разделен екран"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 4add02eec96d..63c9684070b6 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"আনস্ট্যাস করুন"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"অ্যাপটি স্প্লিট স্ক্রিনে কাজ নাও করতে পারে।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"অ্যাপ্লিকেশান বিভক্ত-স্ক্রিন সমর্থন করে না৷"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই অ্যাপটি শুধু ১টি উইন্ডোয় খোলা যেতে পারে।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"সেকেন্ডারি ডিসপ্লেতে অ্যাপটি কাজ নাও করতে পারে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"সেকেন্ডারি ডিসপ্লেতে অ্যাপ লঞ্চ করা যাবে না।"</string> <string name="accessibility_divider" msgid="703810061635792791">"বিভক্ত-স্ক্রিন বিভাজক"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 1444b89e2504..b725efea6e48 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vađenje iz stasha"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi na podijeljenom ekranu."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova aplikacija se može otvoriti samo u 1 prozoru."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog ekrana"</string> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index a0bec428499e..4383916f6597 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Deixa d\'amagar"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"És possible que l\'aplicació no funcioni amb la pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'aplicació no admet la pantalla dividida."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aquesta aplicació només pot obrir-se en 1 finestra."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"És possible que l\'aplicació no funcioni en una pantalla secundària."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'aplicació no es pot obrir en pantalles secundàries."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalles"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 332e9568b6d8..e5cb26f3e3b4 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušit uložení"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikace v režimu rozdělené obrazovky nemusí fungovat."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikace nepodporuje režim rozdělené obrazovky."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Tuto aplikaci lze otevřít jen na jednom okně."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikace na sekundárním displeji nemusí fungovat."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikace nepodporuje spuštění na sekundárních displejích."</string> <string name="accessibility_divider" msgid="703810061635792791">"Čára rozdělující obrazovku"</string> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index a7fb175b69f9..46f7c6985ec2 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vis"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen fungerer muligvis ikke i opdelt skærm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen understøtter ikke opdelt skærm."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denne app kan kun åbnes i 1 vindue."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer muligvis ikke på sekundære skærme."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke åbnes på sekundære skærme."</string> <string name="accessibility_divider" msgid="703810061635792791">"Adskiller til opdelt skærm"</string> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 8d69eb4c9daf..1269d362903d 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Aus Stash entfernen"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen im Modus für geteilten Bildschirm nicht."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Das Teilen des Bildschirms wird in dieser App nicht unterstützt."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Diese App kann nur in einem einzigen Fenster geöffnet werden."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Die App funktioniert auf einem sekundären Display möglicherweise nicht."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Die App unterstützt den Start auf sekundären Displays nicht."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bildschirmteiler"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 613e5b075e0f..f8a69ef796b9 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Κατάργηση απόκρυψης"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Αυτή η εφαρμογή μπορεί να ανοιχθεί μόνο σε 1 παράθυρο."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Η εφαρμογή ίσως να μην λειτουργήσει σε δευτερεύουσα οθόνη."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Η εφαρμογή δεν υποστηρίζει την εκκίνηση σε δευτερεύουσες οθόνες."</string> <string name="accessibility_divider" msgid="703810061635792791">"Διαχωριστικό οθόνης"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 5619617c9f76..8e46c3e0999e 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 9f156697f5c7..7cbbf64991cd 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in 1 window."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split-screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 5619617c9f76..8e46c3e0999e 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 5619617c9f76..8e46c3e0999e 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 2b474db99f60..b2720be3d8a0 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in 1 window."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split-screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 67386c4f31ea..47445a7a0488 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Dejar de almacenar de manera segura"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la app no funcione en el modo de pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La app no es compatible con la función de pantalla dividida."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta app solo puede estar abierta en 1 ventana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la app no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La app no puede iniciarse en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index d4a6b6ce7347..6c45231c3261 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"No esconder"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la aplicación no funcione con la pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La aplicación no admite la pantalla dividida."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta aplicación solo puede abrirse en una ventana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la aplicación no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La aplicación no se puede abrir en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Dividir la pantalla"</string> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index df84e7c923de..a8dc08cbbd27 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Eemalda hoidlast"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Rakendus ei pruugi poolitatud ekraaniga töötada."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Rakendus ei toeta jagatud ekraani."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Selle rakenduse saab avada ainult ühes aknas."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Rakendus ei pruugi teisesel ekraanil töötada."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Rakendus ei toeta teisestel ekraanidel käivitamist."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekraanijagaja"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index c062b928c2ec..e7cb5f41a3ac 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"لغو مخفیسازی"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن است برنامه با «صفحهٔ دونیمه» کار نکند."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"برنامه از تقسیم صفحه پشتیبانی نمیکند."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"این برنامه فقط در ۱ پنجره میتواند باز شود."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن است برنامه در نمایشگر ثانویه کار نکند."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"برنامه از راهاندازی در نمایشگرهای ثانویه پشتیبانی نمیکند."</string> <string name="accessibility_divider" msgid="703810061635792791">"تقسیمکننده صفحه"</string> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 6efb770d2727..86199f3cf092 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poista turvasäilytyksestä"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Sovellus ei ehkä toimi jaetulla näytöllä."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Sovellus ei tue jaetun näytön tilaa."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Tämän sovelluksen voi avata vain yhdessä ikkunassa."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Sovellus ei ehkä toimi toissijaisella näytöllä."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Sovellus ei tue käynnistämistä toissijaisilla näytöillä."</string> <string name="accessibility_divider" msgid="703810061635792791">"Näytön jakaja"</string> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 0c2e1b1298ca..1f3ac9ecbcca 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Retirer de la réserve"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'application n\'est pas compatible avec l\'écran partagé."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Cette application ne peut être ouverte que dans une fenêtre."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index b7147180126f..f1dbb35dc7a7 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Application incompatible avec l\'écran partagé."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Cette appli ne peut être ouverte que dans 1 fenêtre."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 952d5329bcd0..6e215a1f5b81 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Non esconder"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Pode que a aplicación non funcione coa pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A aplicación non é compatible coa función de pantalla dividida."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta aplicación só se pode abrir en 1 ventá."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É posible que a aplicación non funcione nunha pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A aplicación non se pode iniciar en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 67488f994468..1446e70ecdfa 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poništite sakrivanje"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podijeljeni zaslon."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova se aplikacija može otvoriti samo u jednom prozoru."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionirati na sekundarnom zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim zaslonima."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog zaslona"</string> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 41d2d3176994..221c329020a0 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Félretevés megszüntetése"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Lehet, hogy az alkalmazás nem működik osztott képernyős nézetben."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Az alkalmazás nem támogatja az osztott képernyős nézetet."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ez az alkalmazás csak egy ablakban nyitható meg."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Előfordulhat, hogy az alkalmazás nem működik másodlagos kijelzőn."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Az alkalmazást nem lehet másodlagos kijelzőn elindítani."</string> <string name="accessibility_divider" msgid="703810061635792791">"Elválasztó az osztott nézetben"</string> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 502bfb15c932..7be9941d2a5e 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ցուցադրել"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում։"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Հավելվածը չի աջակցում էկրանի տրոհումը:"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Այս հավելվածը հնարավոր է բացել միայն մեկ պատուհանում։"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Հավելվածը կարող է չաշխատել լրացուցիչ էկրանի վրա"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Հավելվածը չի աջակցում գործարկումը լրացուցիչ էկրանների վրա"</string> <string name="accessibility_divider" msgid="703810061635792791">"Տրոհված էկրանի բաժանիչ"</string> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 561df7269bcf..4e760ef6450c 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Batalkan stash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikasi mungkin tidak berfungsi dengan layar terpisah."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App tidak mendukung layar terpisah."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aplikasi ini hanya dapat dibuka di 1 jendela."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikasi mungkin tidak berfungsi pada layar sekunder."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikasi tidak mendukung peluncuran pada layar sekunder."</string> <string name="accessibility_divider" msgid="703810061635792791">"Pembagi layar terpisah"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 355968bfb6eb..50d4ee7faed6 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Taka úr geymslu"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Hugsanlega virkar forritið ekki með skjáskiptingu."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Forritið styður ekki að skjánum sé skipt."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aðeins er hægt að opna þetta forrit í 1 glugga."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Hugsanlegt er að forritið virki ekki á öðrum skjá."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Forrit styður ekki opnun á öðrum skjá."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skjáskipting"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 1417d0021658..d2595f7682d2 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Annulla accantonamento"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"L\'app potrebbe non funzionare con lo schermo diviso."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'app non supporta la modalità Schermo diviso."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Questa app può essere aperta soltanto in 1 finestra."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"L\'app potrebbe non funzionare su un display secondario."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'app non supporta l\'avvio su display secondari."</string> <string name="accessibility_divider" msgid="703810061635792791">"Strumento per schermo diviso"</string> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index b3c0b655ccb1..883596ebcd6f 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ביטול ההסתרה הזמנית"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ייתכן שהאפליקציה לא תפעל במסך מפוצל."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"האפליקציה אינה תומכת במסך מפוצל."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ניתן לפתוח את האפליקציה הזו רק בחלון אחד."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ייתכן שהאפליקציה לא תפעל במסך משני."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"האפליקציה אינה תומכת בהפעלה במסכים משניים."</string> <string name="accessibility_divider" msgid="703810061635792791">"מחלק מסך מפוצל"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index a5b0f21882f7..6bb22a29e79b 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"表示"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"アプリは分割画面では動作しないことがあります。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"アプリで分割画面がサポートされていません。"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"このアプリはウィンドウが 1 つの場合のみ開くことができます。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"アプリはセカンダリ ディスプレイでは動作しないことがあります。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"アプリはセカンダリ ディスプレイでの起動に対応していません。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割画面の分割線"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index a85efb433360..6cf7d7827ee9 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"გადანახვის გაუქმება"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ამ აპის გახსნა შესაძლებელია მხოლოდ 1 ფანჯარაში."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"აპმა შეიძლება არ იმუშაოს მეორეულ ეკრანზე."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"აპს არ გააჩნია მეორეული ეკრანის მხარდაჭერა."</string> <string name="accessibility_divider" msgid="703810061635792791">"გაყოფილი ეკრანის რეჟიმის გამყოფი"</string> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 9c0c15cd6665..216619a232fc 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Көрсету"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Қодланба бөлінген экранды қолдамайды."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бұл қолданбаны тек 1 терезеден ашуға болады."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Қолданба қосымша дисплейде жұмыс істемеуі мүмкін."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Қолданба қосымша дисплейлерде іске қосуды қолдамайды."</string> <string name="accessibility_divider" msgid="703810061635792791">"Бөлінген экран бөлгіші"</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index d56d6a5c5a78..79aca620f348 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ឈប់លាក់ជាបណ្ដោះអាសន្ន"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"កម្មវិធីអាចនឹងមិនដំណើរការជាមួយមុខងារបំបែកអេក្រង់ទេ។"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"កម្មវិធីមិនគាំទ្រអេក្រង់បំបែកជាពីរទេ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"កម្មវិធីនេះអាចបើកនៅក្នុងវិនដូតែ 1 ប៉ុណ្ណោះ។"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"កម្មវិធីនេះប្រហែលជាមិនដំណើរការនៅលើអេក្រង់បន្ទាប់បន្សំទេ។"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"កម្មវិធីនេះមិនអាចចាប់ផ្តើមនៅលើអេក្រង់បន្ទាប់បន្សំបានទេ។"</string> <string name="accessibility_divider" msgid="703810061635792791">"កម្មវិធីចែកអេក្រង់បំបែក"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 1b31a6f883bb..9e9333ea422c 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ಅನ್ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ವಿಭಜಿಸಿದ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ಅಪ್ಲಿಕೇಶನ್ ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ಈ ಆ್ಯಪ್ ಅನ್ನು 1 ವಿಂಡೋದಲ್ಲಿ ಮಾತ್ರ ತೆರೆಯಬಹುದು."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಅಪ್ಲಿಕೇಶನ್ ಕಾರ್ಯ ನಿರ್ವಹಿಸದೇ ಇರಬಹುದು."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಪ್ರಾರಂಭಿಸುವಿಕೆಯನ್ನು ಅಪ್ಲಿಕೇಶನ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> <string name="accessibility_divider" msgid="703810061635792791">"ಸ್ಪ್ಲಿಟ್-ಪರದೆ ಡಿವೈಡರ್"</string> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 563db5b7e7c2..f9b495a38f4f 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"숨기기 취소"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"앱이 분할 화면에서 작동하지 않을 수 있습니다."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"앱이 화면 분할을 지원하지 않습니다."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"이 앱은 창 1개에서만 열 수 있습니다."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"앱이 보조 디스플레이에서 작동하지 않을 수도 있습니다."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"앱이 보조 디스플레이에서의 실행을 지원하지 않습니다."</string> <string name="accessibility_divider" msgid="703810061635792791">"화면 분할기"</string> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 0497d15671ed..a57f9ae9689f 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Сейфтен чыгаруу"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Колдонмодо экран бөлүнбөшү мүмкүн."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Колдонмодо экран бөлүнбөйт."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бул колдонмону 1 терезеде гана ачууга болот."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Колдонмо кошумча экранда иштебей коюшу мүмкүн."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Колдонмону кошумча экрандарда иштетүүгө болбойт."</string> <string name="accessibility_divider" msgid="703810061635792791">"Экранды бөлгүч"</string> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 35aecaaada6c..8e42aa346669 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ເອົາອອກຈາກບ່ອນເກັບສ່ວນຕົວ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບການແບ່ງໜ້າຈໍ."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ແອັບບໍ່ຮອງຮັບໜ້າຈໍແບບແຍກກັນ."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ແອັບນີ້ສາມາດເປີດໄດ້ໃນ 1 ໜ້າຈໍເທົ່ານັ້ນ."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ແອັບອາດບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍທີສອງ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ແອັບບໍ່ຮອງຮັບການເປີດໃນໜ້າຈໍທີສອງ."</string> <string name="accessibility_divider" msgid="703810061635792791">"ຕົວຂັ້ນການແບ່ງໜ້າຈໍ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index f4ed192f2fa8..dc9969095cf5 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Nebeslėpti"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Programa gali neveikti naudojant išskaidyto ekrano režimą."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programoje nepalaikomas skaidytas ekranas."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Šią programą galima atidaryti tik viename lange."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Programa gali neveikti antriniame ekrane."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programa nepalaiko paleisties antriniuose ekranuose."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skaidyto ekrano daliklis"</string> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index ffb9ddb9f4b0..bd2eef42690b 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Rādīt"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Lietotnē netiek atbalstīta ekrāna sadalīšana."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Šo lietotni var atvērt tikai vienā logā."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Lietotne, iespējams, nedarbosies sekundārajā displejā."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Lietotnē netiek atbalstīta palaišana sekundārajos displejos."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekrāna sadalītājs"</string> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index cf6b9d5bf627..d133654ba777 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Прикажете"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликацијата може да не работи со поделен екран."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликацијата не поддржува поделен екран."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Апликацијава може да се отвори само во еден прозорец."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликацијата може да не функционира на друг екран."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликацијата не поддржува стартување на други екрани."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделник на поделен екран"</string> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 54513d1866ee..264f9a0a4db9 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ил гаргах"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апп хуваагдсан дэлгэц дээр ажиллахгүй байж болзошгүй."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Энэ апп нь дэлгэц хуваах тохиргоог дэмждэггүй."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Энэ аппыг зөвхөн 1 цонхонд нээх боломжтой."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апп хоёрдогч дэлгэцэд ажиллахгүй."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Аппыг хоёрдогч дэлгэцэд эхлүүлэх боломжгүй."</string> <string name="accessibility_divider" msgid="703810061635792791">"\"Дэлгэц хуваах\" хуваагч"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 283348028979..7a475edcd964 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्टॅश करा"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"अॅप कदाचित स्प्लिट स्क्रीनसह काम करू शकत नाही."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अॅप स्क्रीन-विभाजनास समर्थन देत नाही."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"हे अॅप फक्त एका विंडोमध्ये उघडले जाऊ शकते."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"दुसऱ्या डिस्प्लेवर अॅप कदाचित चालणार नाही."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"दुसऱ्या डिस्प्लेवर अॅप लाँच होणार नाही."</string> <string name="accessibility_divider" msgid="703810061635792791">"विभाजित-स्क्रीन विभाजक"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 20f8ff69ebc1..be1dc24ccfcb 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Tunjukkan"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Apl mungkin tidak berfungsi dengan skrin pisah."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Apl tidak menyokong skrin pisah."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Apl ini hanya boleh dibuka dalam 1 tetingkap."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Apl mungkin tidak berfungsi pada paparan kedua."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Apl tidak menyokong pelancaran pada paparan kedua."</string> <string name="accessibility_divider" msgid="703810061635792791">"Pembahagi skrin pisah"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index dd2750b339a2..3e18b4968464 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Avslutt oppbevaring"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Det kan hende at appen ikke fungerer med delt skjerm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen støtter ikke delt skjerm."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denne appen kan bare åpnes i ett vindu."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer kanskje ikke på en sekundær skjerm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke kjøres på sekundære skjermer."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skilleelement for delt skjerm"</string> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 054f05f4bb8d..b056483e3b2d 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Niet meer verbergen"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"De app werkt mogelijk niet met gesplitst scherm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App biedt geen ondersteuning voor gesplitst scherm."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Deze app kan slechts in 1 venster worden geopend."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App werkt mogelijk niet op een secundair scherm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App kan niet op secundaire displays worden gestart."</string> <string name="accessibility_divider" msgid="703810061635792791">"Scheiding voor gesplitst scherm"</string> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index d0971a35e622..5fd81f44bf0d 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ଦେଖାନ୍ତୁ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରିନରେ ଆପ୍ କାମ କରିନପାରେ।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ଆପ୍ ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନକୁ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ଏହି ଆପକୁ କେବଳ 1ଟି ୱିଣ୍ଡୋରେ ଖୋଲାଯାଇପାରିବ।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ କାମ ନକରିପାରେ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ ଲଞ୍ଚ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> <string name="accessibility_divider" msgid="703810061635792791">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନ ବିଭାଜକ"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 989f8fce4f50..ada79d195df2 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ਅਣਸਟੈਸ਼"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ।"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ਇਹ ਐਪ ਸਿਰਫ਼ 1 ਵਿੰਡੋ ਵਿੱਚ ਖੋਲ੍ਹੀ ਜਾ ਸਕਦੀ ਹੈ।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇਆਂ \'ਤੇ ਲਾਂਚ ਕਰਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string> <string name="accessibility_divider" msgid="703810061635792791">"ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਡਿਵਾਈਡਰ"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 3458f261dfdd..a97fd5c9ddb0 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zabierz ze schowka"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacja może nie działać przy podzielonym ekranie."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacja nie obsługuje dzielonego ekranu."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ta aplikacja może być otwarta tylko w 1 oknie."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacja może nie działać na dodatkowym ekranie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacja nie obsługuje uruchamiania na dodatkowych ekranach."</string> <string name="accessibility_divider" msgid="703810061635792791">"Linia dzielenia ekranu"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index aeb7f9a296b3..e0636d42d980 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Remover do armazenamento"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"A app pode não funcionar com o ecrã dividido."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A app não é compatível com o ecrã dividido."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta app só pode ser aberta em 1 janela."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"A app pode não funcionar num ecrã secundário."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A app não é compatível com o início em ecrãs secundários."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor do ecrã dividido"</string> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 23b209940490..9227216ffc1f 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aplicația poate fi deschisă într-o singură fereastră."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplicația nu acceptă lansare pe ecrane secundare."</string> <string name="accessibility_divider" msgid="703810061635792791">"Separator pentru ecranul împărțit"</string> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index c1dfb19162fe..f5fa1a475886 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показать"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"В режиме разделения экрана приложение может работать нестабильно."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложение не поддерживает разделение экрана."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Это приложение можно открыть только в одном окне."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Приложение может не работать на дополнительном экране"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложение не поддерживает запуск на дополнительных экранах"</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделитель экрана"</string> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 5e51003e4429..53e52f28adcf 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"සඟවා තැබීම ඉවත් කරන්න"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"යෙදුම බෙදුම් තිරය සමග ක්රියා නොකළ හැකිය"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"යෙදුම බෙදුණු-තිරය සඳහා සහාය නොදක්වයි."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"මෙම යෙදුම විවෘත කළ හැක්කේ 1 කවුළුවක පමණයි."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"යෙදුම ද්විතියික සංදර්ශකයක ක්රියා නොකළ හැකිය."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"යෙදුම ද්විතීයික සංදර්ශක මත දියත් කිරීම සඳහා සහාය නොදක්වයි."</string> <string name="accessibility_divider" msgid="703810061635792791">"බෙදුම්-තිර වෙන්කරණය"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index baf03241dcc6..f004fd472adf 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušiť skrytie"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikácia nemusí fungovať s rozdelenou obrazovkou."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikácia nepodporuje rozdelenú obrazovku."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Táto aplikácia môže byť otvorená iba v jednom okne."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikácia nemusí fungovať na sekundárnej obrazovke."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikácia nepodporuje spúšťanie na sekundárnych obrazovkách."</string> <string name="accessibility_divider" msgid="703810061635792791">"Rozdeľovač obrazovky"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 2a12a64b6076..c4808059a0c4 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Razkrij"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podpira načina razdeljenega zaslona."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"To aplikacijo je mogoče odpreti samo v enem oknu."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija morda ne bo delovala na sekundarnem zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podpira zagona na sekundarnih zaslonih."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdelilnik zaslonov"</string> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index eb0199f55eed..b59d4db6413e 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Mos e fshih"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacioni mund të mos funksionojë me ekranin e ndarë."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacioni nuk mbështet ekranin e ndarë."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ky aplikacion mund të hapet vetëm në 1 dritare."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacioni mund të mos funksionojë në një ekran dytësor."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacioni nuk mbështet nisjen në ekrane dytësore."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ndarësi i ekranit të ndarë"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 6e6019c160fe..78d74d74ac3f 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Уклоните из тајне меморије"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликација можда неће радити са подељеним екраном."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликација не подржава подељени екран."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ова апликација може да се отвори само у једном прозору."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликација можда неће функционисати на секундарном екрану."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликација не подржава покретање на секундарним екранима."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделник подељеног екрана"</string> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 419c31d3fb83..cda3040dc056 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Återställ stash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen kanske inte fungerar med delad skärm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen har inte stöd för delad skärm."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denna app kan bara vara öppen i ett fönster."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen kanske inte fungerar på en sekundär skärm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan inte köras på en sekundär skärm."</string> <string name="accessibility_divider" msgid="703810061635792791">"Avdelare för delad skärm"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 0c9b96d5e36c..f0c8be5c5957 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ఆన్స్టాచ్"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"స్క్రీన్ విభజనతో యాప్ పని చేయకపోవచ్చు."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"యాప్లో స్క్రీన్ విభజనకు మద్దతు లేదు."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ఈ యాప్ను 1 విండోలో మాత్రమే తెరవవచ్చు."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ప్రత్యామ్నాయ డిస్ప్లేలో యాప్ పని చేయకపోవచ్చు."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ప్రత్యామ్నాయ డిస్ప్లేల్లో ప్రారంభానికి యాప్ మద్దతు లేదు."</string> <string name="accessibility_divider" msgid="703810061635792791">"విభజన స్క్రీన్ విభాగిని"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 94abe5b97845..2437e0377780 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"เอาออกจากที่เก็บส่วนตัว"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"แอปอาจใช้ไม่ได้กับโหมดแบ่งหน้าจอ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"แอปไม่สนับสนุนการแยกหน้าจอ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"แอปนี้เปิดได้ใน 1 หน้าต่างเท่านั้น"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"แอปอาจไม่ทำงานในจอแสดงผลรอง"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"แอปไม่รองรับการเรียกใช้ในจอแสดงผลรอง"</string> <string name="accessibility_divider" msgid="703810061635792791">"เส้นแบ่งหน้าจอ"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 589bedd023c5..86ef75718b77 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"I-unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Posibleng hindi gumana ang app sa split screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Hindi sinusuportahan ng app ang split-screen."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Sa 1 window lang puwedeng buksan ang app na ito."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Maaaring hindi gumana ang app sa pangalawang display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Hindi sinusuportahan ng app ang paglulunsad sa mga pangalawang display."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divider ng split-screen"</string> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 6b73f2f7ed7b..c4060cc795ab 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Depolama"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Uygulama bölünmüş ekranda çalışmayabilir."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uygulama bölünmüş ekranı desteklemiyor."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu uygulama yalnızca 1 pencerede açılabilir."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uygulama ikincil ekranda çalışmayabilir."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uygulama ikincil ekranlarda başlatılmayı desteklemiyor."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcı"</string> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 50908cc24fbd..ca6a93714cdd 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے۔"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ایپ سپلٹ اسکرین کو سپورٹ نہیں کرتی۔"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"یہ ایپ صرف 1 ونڈو میں کھولی جا سکتی ہے۔"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن ہے ایپ ثانوی ڈسپلے پر کام نہ کرے۔"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ایپ ثانوی ڈسپلیز پر شروعات کا تعاون نہیں کرتی۔"</string> <string name="accessibility_divider" msgid="703810061635792791">"سپلٹ اسکرین تقسیم کار"</string> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index e0b802e6b0b9..8f173d5d1e4b 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Chiqarish"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Bu ilova ekranni ikkiga ajratish rejimini dastaklamaydi."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Bu ilova ekranni bo‘lish xususiyatini qo‘llab-quvvatlamaydi."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu ilovani faqat 1 ta oynada ochish mumkin."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Bu ilova qo‘shimcha ekranda ishlamasligi mumkin."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Bu ilova qo‘shimcha ekranlarda ishga tushmaydi."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekranni ikkiga bo‘lish chizig‘i"</string> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 71c3632714f1..1d5b9d63ee5a 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Hiện"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Ứng dụng có thể không hoạt động với tính năng chia đôi màn hình."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Ứng dụng không hỗ trợ chia đôi màn hình."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ứng dụng này chỉ có thể mở 1 cửa sổ."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Ứng dụng có thể không hoạt động trên màn hình phụ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Ứng dụng không hỗ trợ khởi chạy trên màn hình phụ."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bộ chia chia đôi màn hình"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 35ff2f22be03..87f2973aa618 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消隐藏"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"应用可能无法在分屏模式下正常运行。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"应用不支持分屏。"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此应用只能在 1 个窗口中打开。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"应用可能无法在辅显示屏上正常运行。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"应用不支持在辅显示屏上启动。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分屏分隔线"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 9c17f64b881f..f9b22d226c4f 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消保護"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"應用程式不支援分割畫面。"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此應用程式只可在 1 個視窗中開啟"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示屏上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示屏上啟動。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index c4d9ca467a82..1438e52ccb4a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消暫時隱藏"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"這個應用程式不支援分割畫面。"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"這個應用程式只能在 1 個視窗中開啟。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示器上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示器上啟動。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 4d8f7e33d47e..e9238dc0833a 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -33,8 +33,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Susa isiteshi"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Izinhlelo zokusebenza kungenzeka zingasebenzi ngesikrini esihlukanisiwe."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uhlelo lokusebenza alusekeli isikrini esihlukanisiwe."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Le-app ingavulwa kuphela ewindini eli-1."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uhlelo lokusebenza kungenzeka lungasebenzi kusibonisi sesibili."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uhlelo lokusebenza alusekeli ukuqalisa kuzibonisi zesibili."</string> <string name="accessibility_divider" msgid="703810061635792791">"Isihlukanisi sokuhlukanisa isikrini"</string> 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 bec6844ae863..dd8afff0df2c 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 @@ -475,8 +475,13 @@ public class BubbleController implements ConfigurationChangeListener { @VisibleForTesting public void onStatusBarStateChanged(boolean isShade) { + boolean didChange = mIsStatusBarShade != isShade; + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, "onStatusBarStateChanged isShade=" + isShade + " didChange=" + didChange); + } mIsStatusBarShade = isShade; - if (!mIsStatusBarShade) { + if (!mIsStatusBarShade && didChange) { + // Only collapse stack on change collapseStack(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index e8b0f0265394..214b304df07c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -219,7 +219,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); // Only insets the divider bar with task bar when it's expanded so that the rounded corners // will be drawn against task bar. - if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + // But there is no need to do it when IME showing because there are no rounded corners at + // the bottom. This also avoids the problem of task bar height not changing when IME + // floating. + if (!insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME) + && taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index ae496165028f..45b234a6398a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -122,6 +122,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mDensity; private final boolean mDimNonImeSide; + private ValueAnimator mDividerFlingAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, @@ -395,6 +396,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitWindowManager.release(t); mDisplayImeController.removePositionProcessor(mImePositionProcessor); mImePositionProcessor.reset(); + if (mDividerFlingAnimator != null) { + mDividerFlingAnimator.cancel(); + } + resetDividerPosition(); } public void release() { @@ -577,13 +582,18 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange CUJ_SPLIT_SCREEN_RESIZE); return; } - ValueAnimator animator = ValueAnimator + + if (mDividerFlingAnimator != null) { + mDividerFlingAnimator.cancel(); + } + + mDividerFlingAnimator = ValueAnimator .ofInt(from, to) .setDuration(duration); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addUpdateListener( + mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + mDividerFlingAnimator.addUpdateListener( animation -> updateDivideBounds((int) animation.getAnimatedValue())); - animator.addListener(new AnimatorListenerAdapter() { + mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (flingFinishedCallback != null) { @@ -591,14 +601,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } InteractionJankMonitorUtils.endTracing( CUJ_SPLIT_SCREEN_RESIZE); + mDividerFlingAnimator = null; } @Override public void onAnimationCancel(Animator animation) { - setDividePosition(to, true /* applyLayoutChange */); + mDividerFlingAnimator = null; } }); - animator.start(); + mDividerFlingAnimator.start(); } /** Switch both surface position with animation. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 90b35a5a55e1..44bcdb2d5de5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -82,7 +82,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { mTasks.put(taskInfo.taskId, state); if (!Transitions.ENABLE_SHELL_TRANSITIONS) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t); + mWindowDecorationViewModel.onTaskOpening(taskInfo, leash, t, t); t.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 168f6d79a390..6e710f7caeda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -120,7 +120,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mWindowDecorViewModel.createWindowDecoration( + mWindowDecorViewModel.onTaskOpening( change.getTaskInfo(), change.getLeash(), startT, finishT); } @@ -128,31 +128,23 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mWindowDecorViewModel.setupWindowDecorationForTransition( - change.getTaskInfo(), startT, finishT); + mWindowDecorViewModel.onTaskClosing(change.getTaskInfo(), startT, finishT); } private void onChangeTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mWindowDecorViewModel.setupWindowDecorationForTransition( - change.getTaskInfo(), startT, finishT); + mWindowDecorViewModel.onTaskChanging( + change.getTaskInfo(), change.getLeash(), startT, finishT); } private void onToFrontTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - boolean exists = mWindowDecorViewModel.setupWindowDecorationForTransition( - change.getTaskInfo(), - startT, - finishT); - if (!exists) { - // Window caption does not exist, create it - mWindowDecorViewModel.createWindowDecoration( - change.getTaskInfo(), change.getLeash(), startT, finishT); - } + mWindowDecorViewModel.onTaskChanging( + change.getTaskInfo(), change.getLeash(), startT, finishT); } @Override @@ -188,4 +180,4 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i)); } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 75a4091c7d78..6623f5ca84ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -103,7 +103,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { if (mWindowDecorViewModelOptional.isPresent()) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); createdWindowDecor = mWindowDecorViewModelOptional.get() - .createWindowDecoration(taskInfo, leash, t, t); + .onTaskOpening(taskInfo, leash, t, t); t.apply(); } if (!createdWindowDecor) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f170e774739f..8ba2583757cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -63,6 +63,7 @@ import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; +import android.view.Choreographer; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; @@ -179,8 +180,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // This is necessary in case there was a resize animation ongoing when exit PIP // started, in which case the first resize will be skipped to let the exit // operation handle the final resize out of PIP mode. See b/185306679. - finishResize(tx, destinationBounds, direction, animationType); - sendOnPipTransitionFinished(direction); + finishResizeDelayedIfNeeded(() -> { + finishResize(tx, destinationBounds, direction, animationType); + sendOnPipTransitionFinished(direction); + }); } } @@ -196,6 +199,39 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } }; + /** + * Finishes resizing the PiP, delaying the operation if it has to be synced with the PiP menu. + * + * This is done to avoid a race condition between the last transaction applied in + * onPipAnimationUpdate and the finishResize in onPipAnimationEnd. The transaction in + * onPipAnimationUpdate is applied directly from WmShell, while onPipAnimationEnd creates a + * WindowContainerTransaction in finishResize, which is to be applied by WmCore later. Normally, + * the WCT should be the last transaction to finish the animation. However, it may happen that + * it gets applied *before* the transaction created by the last onPipAnimationUpdate. This + * happens only when the PiP surface transaction has to be synced with the PiP menu due to the + * necessity for a delay when syncing the PiP surface animation with the PiP menu surface + * animation and redrawing the PiP menu contents. As a result, the PiP surface gets scaled after + * the new bounds are applied by WmCore, which makes the PiP surface have unexpected bounds. + * + * To avoid this, we delay the finishResize operation until + * the next frame. This aligns the last onAnimationUpdate transaction with the WCT application. + */ + private void finishResizeDelayedIfNeeded(Runnable finishResizeRunnable) { + if (!shouldSyncPipTransactionWithMenu()) { + finishResizeRunnable.run(); + return; + } + + // Delay the finishResize to the next frame + Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { + mMainExecutor.execute(finishResizeRunnable); + }, null); + } + + private boolean shouldSyncPipTransactionWithMenu() { + return mPipMenuController.isMenuVisible(); + } + @VisibleForTesting final PipTransitionController.PipTransitionCallback mPipTransitionCallback = new PipTransitionController.PipTransitionCallback() { @@ -221,7 +257,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Override public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, Rect destinationBounds) { - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(leash, tx, destinationBounds); return true; } @@ -1223,7 +1259,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { tx.apply(); @@ -1265,7 +1301,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .scale(tx, mLeash, startBounds, toBounds, degrees) .round(tx, mLeash, startBounds, toBounds); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(mLeash, tx, toBounds); } else { tx.apply(); 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 d28a9f3cf8ff..efe938f0a274 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 @@ -612,12 +612,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb new DisplayInsetsController.OnInsetsChangedListener() { @Override public void insetsChanged(InsetsState insetsState) { + DisplayLayout pendingLayout = + mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()); + if (mIsInFixedRotation + || pendingLayout.rotation() + != mPipBoundsState.getDisplayLayout().rotation()) { + // bail out if there is a pending rotation or fixed rotation change + return; + } int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; onDisplayChanged( mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()), false /* saveRestoreSnapFraction */); int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; if (!mEnablePipKeepClearAlgorithm) { + // offset PiP to adjust for bottom inset change int pipTop = mPipBoundsState.getBounds().top; int diff = newMaxMovementBound - oldMaxMovementBound; if (diff < 0 && pipTop > newMaxMovementBound) { 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 acb71a80ee8a..4cb76230606f 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 @@ -669,6 +669,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.init(); mSplitLayout.setDivideRatio(splitRatio); + // Apply surface bounds before animation start. + SurfaceControl.Transaction startT = mTransactionPool.acquire(); + updateSurfaceBounds(mSplitLayout, startT, false /* applyResizingOffset */); + startT.apply(); + mTransactionPool.release(startT); + // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; mIsDividerRemoteAnimating = true; @@ -742,7 +748,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); }); setEnterInstanceId(instanceId); @@ -1035,7 +1040,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mIsDividerRemoteAnimating = false; mSplitLayout.getInvisibleBounds(mTempRect1); - if (childrenToTop == null) { + if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) { mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); @@ -1294,13 +1299,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - private void onStageChildTaskEnterPip() { - // When the exit split-screen is caused by one of the task enters auto pip, - // we want both tasks to be put to bottom instead of top, otherwise it will end up - // a fullscreen plus a pinned task instead of pinned only at the end of the transition. - exitSplitScreen(null, EXIT_REASON_CHILD_TASK_ENTER_PIP); - } - private void updateRecentTasksSplitPair() { if (!mShouldUpdateRecents) { return; @@ -2063,7 +2061,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Update divider state after animation so that it is still around and positioned // properly for the animation itself. mSplitLayout.release(); - mSplitLayout.resetDividerPosition(); mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; } } @@ -2340,11 +2337,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onChildTaskEnterPip() { - StageCoordinator.this.onStageChildTaskEnterPip(); - } - - @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onRootTaskVanished(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index bcf900b99c69..358f712f76b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.RemoteAnimationTarget.MODE_OPENING; @@ -74,8 +73,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); - void onChildTaskEnterPip(); - void onRootTaskVanished(); void onNoLongerSupportMultiWindow(); @@ -257,9 +254,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { // Status is managed/synchronized by the transition lifecycle. return; } - if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { - mCallbacks.onChildTaskEnterPip(); - } sendStatusChanged(); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 4e1fa290270d..485b400f458d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -77,10 +77,10 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { if (mRemote.asBinder() != null) { mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } + if (sct != null) { + finishTransaction.merge(sct); + } mMainExecutor.execute(() -> { - if (sct != null) { - finishTransaction.merge(sct); - } finishCallback.onTransitionFinished(wct, null /* wctCB */); }); } @@ -90,7 +90,13 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteStartT = RemoteTransitionHandler.copyIfLocal( + startTransaction, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = + remoteStartT == startTransaction ? info : info.localRemoteCopy(); + mRemote.getRemoteTransition().startAnimation(transition, remoteInfo, remoteStartT, cb); // assume that remote will apply the start transaction. startTransaction.clear(); } catch (RemoteException e) { @@ -124,7 +130,13 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { } }; try { - mRemote.getRemoteTransition().mergeAnimation(transition, info, t, mergeTarget, cb); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteT = + RemoteTransitionHandler.copyIfLocal(t, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + mRemote.getRemoteTransition().mergeAnimation( + transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error merging remote transition.", e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 9469529de8f1..b4e05848882c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -19,6 +19,7 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.IBinder; +import android.os.Parcel; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; @@ -120,10 +121,10 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { public void onTransitionFinished(WindowContainerTransaction wct, SurfaceControl.Transaction sct) { unhandleDeath(remote.asBinder(), finishCallback); + if (sct != null) { + finishTransaction.merge(sct); + } mMainExecutor.execute(() -> { - if (sct != null) { - finishTransaction.merge(sct); - } mRequestedRemotes.remove(transition); finishCallback.onTransitionFinished(wct, null /* wctCB */); }); @@ -131,8 +132,14 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { }; Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread()); try { + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteStartT = + copyIfLocal(startTransaction, remote.getRemoteTransition()); + final TransitionInfo remoteInfo = + remoteStartT == startTransaction ? info : info.localRemoteCopy(); handleDeath(remote.asBinder(), finishCallback); - remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); + remote.getRemoteTransition().startAnimation(transition, remoteInfo, remoteStartT, cb); // assume that remote will apply the start transaction. startTransaction.clear(); } catch (RemoteException e) { @@ -145,6 +152,28 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { return true; } + static SurfaceControl.Transaction copyIfLocal(SurfaceControl.Transaction t, + IRemoteTransition remote) { + // We care more about parceling than local (though they should be the same); so, use + // queryLocalInterface since that's what Binder uses to decide if it needs to parcel. + if (remote.asBinder().queryLocalInterface(IRemoteTransition.DESCRIPTOR) == null) { + // No local interface, so binder itself will parcel and thus we don't need to. + return t; + } + // Binder won't be parceling; however, the remotes assume they have their own native + // objects (and don't know if caller is local or not), so we need to make a COPY here so + // that the remote can clean it up without clearing the original transaction. + // Since there's no direct `copy` for Transaction, we have to parcel/unparcel instead. + final Parcel p = Parcel.obtain(); + try { + t.writeToParcel(p, 0); + p.setDataPosition(0); + return SurfaceControl.Transaction.CREATOR.createFromParcel(p); + } finally { + p.recycle(); + } + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -175,7 +204,11 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } }; try { - remote.mergeAnimation(transition, info, t, mergeTarget, cb); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteT = copyIfLocal(t, remote); + final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + remote.mergeAnimation(transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 857decf65567..b714d2e5e1e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -500,6 +500,7 @@ public class Transitions implements RemoteCallable<Transitions> { // Treat this as an abort since we are bypassing any merge logic and effectively // finishing immediately. onAbort(transitionToken); + releaseSurfaces(info); return; } @@ -604,6 +605,15 @@ public class Transitions implements RemoteCallable<Transitions> { onFinish(transition, wct, wctCB, false /* abort */); } + /** + * Releases an info's animation-surfaces. These don't need to persist and we need to release + * them asap so that SF can free memory sooner. + */ + private void releaseSurfaces(@Nullable TransitionInfo info) { + if (info == null) return; + info.releaseAnimSurfaces(); + } + private void onFinish(IBinder transition, @Nullable WindowContainerTransaction wct, @Nullable WindowContainerTransactionCallback wctCB, @@ -642,6 +652,11 @@ public class Transitions implements RemoteCallable<Transitions> { } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished (abort=%b), notifying core %s", abort, transition); + if (active.mStartT != null) { + // Applied by now, so close immediately. Do not set to null yet, though, since nullness + // is used later to disambiguate malformed transitions. + active.mStartT.close(); + } // Merge all relevant transactions together SurfaceControl.Transaction fullFinish = active.mFinishT; for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { @@ -661,12 +676,14 @@ public class Transitions implements RemoteCallable<Transitions> { fullFinish.apply(); } // Now perform all the finishes. + releaseSurfaces(active.mInfo); mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(transition, wct, wctCB); while (activeIdx < mActiveTransitions.size()) { if (!mActiveTransitions.get(activeIdx).mMerged) break; ActiveTransition merged = mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); + releaseSurfaces(merged.mInfo); } // sift through aborted transitions while (mActiveTransitions.size() > activeIdx @@ -679,8 +696,9 @@ public class Transitions implements RemoteCallable<Transitions> { } mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); for (int i = 0; i < mObservers.size(); ++i) { - mObservers.get(i).onTransitionFinished(active.mToken, true); + mObservers.get(i).onTransitionFinished(aborted.mToken, true); } + releaseSurfaces(aborted.mInfo); } if (mActiveTransitions.size() <= activeIdx) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " 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 index 8369569b4163..e40db4e4dcf2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -55,6 +55,8 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.transition.Transitions; +import java.util.function.Supplier; + /** * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. @@ -62,6 +64,8 @@ import com.android.wm.shell.transition.Transitions; public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private static final String TAG = "CaptionViewModel"; + private final CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; + private final Supplier<InputManager> mInputManagerSupplier; private final ActivityTaskManager mActivityTaskManager; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -77,6 +81,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); + private EventReceiverFactory mEventReceiverFactory = new EventReceiverFactory(); public CaptionWindowDecorViewModel( Context context, @@ -86,6 +91,29 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { DisplayController displayController, SyncTransactionQueue syncQueue, DesktopModeController desktopModeController) { + this( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue, + desktopModeController, + new CaptionWindowDecoration.Factory(), + InputManager::getInstance); + } + + public CaptionWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + DesktopModeController desktopModeController, + CaptionWindowDecoration.Factory captionWindowDecorFactory, + Supplier<InputManager> inputManagerSupplier) { + mContext = context; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; @@ -94,7 +122,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDisplayController = displayController; mSyncQueue = syncQueue; mDesktopModeController = desktopModeController; - mTransitionDragActive = false; + + mCaptionWindowDecorFactory = captionWindowDecorFactory; + mInputManagerSupplier = inputManagerSupplier; + } + + void setEventReceiverFactory(EventReceiverFactory eventReceiverFactory) { + mEventReceiverFactory = eventReceiverFactory; } @Override @@ -103,42 +137,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override - public boolean createWindowDecoration( + public boolean onTaskOpening( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { if (!shouldShowWindowDecor(taskInfo)) return false; - 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); - - TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration, - mDragStartListener); - CaptionTouchEventListener touchEventListener = - new CaptionTouchEventListener(taskInfo, taskPositioner, - windowDecoration.getDragDetector()); - windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); - windowDecoration.setDragResizeCallback(taskPositioner); - setupWindowDecorationForTransition(taskInfo, startT, finishT); - if (mInputMonitor == null) { - mInputMonitor = InputManager.getInstance().monitorGestureInput( - "caption-touch", mContext.getDisplayId()); - mEventReceiver = new EventReceiver( - mInputMonitor.getInputChannel(), Looper.myLooper()); - } + createWindowDecoration(taskInfo, taskSurface, startT, finishT); return true; } @@ -151,25 +156,45 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override - public boolean setupWindowDecorationForTransition( + 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 false; + if (decoration == null) return; decoration.relayout(taskInfo, startT, finishT); - return true; } @Override - public boolean destroyWindowDecoration(RunningTaskInfo taskInfo) { + public void destroyWindowDecoration(RunningTaskInfo taskInfo) { final CaptionWindowDecoration decoration = mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); - if (decoration == null) return false; + if (decoration == null) return; decoration.close(); - return true; } private class CaptionTouchEventListener implements @@ -217,6 +242,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { decoration.setButtonVisibility(); } } + private void injectBackKey() { sendBackEvent(KeyEvent.ACTION_DOWN); sendBackEvent(KeyEvent.ACTION_UP); @@ -266,7 +292,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { */ private void handleEventForMove(MotionEvent e) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - int windowingMode = mDesktopModeController + int windowingMode = mDesktopModeController .getDisplayAreaWindowingMode(taskInfo.displayId); if (windowingMode == WINDOWING_MODE_FULLSCREEN) { return; @@ -302,7 +328,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } // InputEventReceiver to listen for touch input outside of caption bounds - private class EventReceiver extends InputEventReceiver { + class EventReceiver extends InputEventReceiver { EventReceiver(InputChannel channel, Looper looper) { super(channel, looper); } @@ -318,8 +344,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } + class EventReceiverFactory { + EventReceiver create(InputChannel channel, Looper looper) { + return new EventReceiver(channel, looper); + } + } + /** * Handle MotionEvents relevant to focused task's caption that don't directly touch it + * * @param ev the {@link MotionEvent} received by {@link EventReceiver} */ private void handleReceivedMotionEvent(MotionEvent ev) { @@ -401,7 +434,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return focusedDecor; } - private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; return DesktopModeStatus.IS_SUPPORTED @@ -410,7 +442,47 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { .getResources().getConfiguration().smallestScreenWidthDp >= 600; } - private class DragStartListenerImpl implements TaskPositioner.DragStartListener{ + private void createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + 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 = + mCaptionWindowDecorFactory.create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + taskSurface, + mMainHandler, + mMainChoreographer, + mSyncQueue); + mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); + + TaskPositioner taskPositioner = + new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener); + CaptionTouchEventListener touchEventListener = + new CaptionTouchEventListener( + taskInfo, taskPositioner, windowDecoration.getDragDetector()); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); + windowDecoration.setDragResizeCallback(taskPositioner); + windowDecoration.relayout(taskInfo, startT, finishT); + if (mInputMonitor == null) { + InputManager inputManager = mInputManagerSupplier.get(); + mInputMonitor = + inputManager.monitorGestureInput("caption-touch", mContext.getDisplayId()); + mEventReceiver = + mEventReceiverFactory.create( + mInputMonitor.getInputChannel(), Looper.myLooper()); + } + } + + private class DragStartListenerImpl implements TaskPositioner.DragStartListener { @Override public void onDragStart(int taskId) { mWindowDecorByTaskId.get(taskId).closeHandleMenu(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 59576cd3ec15..037ca2031254 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -42,7 +42,8 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with - * {@link CaptionWindowDecorViewModel}. The caption bar contains a handle, back button, and close button. + * {@link CaptionWindowDecorViewModel}. The caption bar contains a handle, back button, and close + * button. * * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. */ @@ -181,12 +182,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { closeDragResizeListener(); mDragResizeListener = new DragResizeInputListener( - mContext, - mHandler, - mChoreographer, - mDisplay.getDisplayId(), - mDecorationContainerSurface, - mDragResizeCallback); + mContext, + mHandler, + mChoreographer, + mDisplay.getDisplayId(), + mDecorationContainerSurface, + mDragResizeCallback); } int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); @@ -242,7 +243,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL /** * Sets the visibility of buttons and color of caption based on desktop mode status - * */ void setButtonVisibility() { mDesktopActive = DesktopModeStatus.isActive(mContext); @@ -313,6 +313,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL /** * Close an open handle menu if input is outside of menu coordinates + * * @param ev the tapped point to compare against */ void closeHandleMenuIfNeeded(MotionEvent ev) { @@ -329,6 +330,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL /** * Offset the coordinates of a {@link MotionEvent} to be in the same coordinate space as caption + * * @param ev the {@link MotionEvent} to offset * @return the point of the input in local space */ @@ -343,7 +345,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL /** * Determine if a passed MotionEvent is in a view in caption - * @param ev the {@link MotionEvent} to check + * + * @param ev the {@link MotionEvent} to check * @param layoutId the id of the view * @return {@code true} if event is inside the specified view, {@code false} if not */ @@ -363,6 +366,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL * Check a passed MotionEvent if a click has occurred on any button on this caption * Note this should only be called when a regular onClick is not possible * (i.e. the button was clicked through status bar layer) + * * @param ev the MotionEvent to compare */ void checkClickEvent(MotionEvent ev) { @@ -399,4 +403,27 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL closeHandleMenu(); super.close(); } + + static class Factory { + + CaptionWindowDecoration create( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Handler handler, + Choreographer choreographer, + SyncTransactionQueue syncQueue) { + return new CaptionWindowDecoration( + context, + displayController, + taskOrganizer, + taskInfo, + taskSurface, + handler, + choreographer, + syncQueue); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 2ce4d04377a1..907977c661f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -28,7 +28,6 @@ import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; * servers. */ public interface WindowDecorViewModel { - /** * Sets the transition starter that starts freeform task transitions. * @@ -37,16 +36,16 @@ public interface WindowDecorViewModel { void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter); /** - * Creates a window decoration for the given task. - * Can be {@code null} for Fullscreen tasks but not Freeform ones. + * Creates a window decoration for the given task. Can be {@code null} for Fullscreen tasks but + * not Freeform ones. * - * @param taskInfo the initial task info of the task + * @param taskInfo the initial task info of the task * @param taskSurface the surface of the task - * @param startT the start transaction to be applied before the transition - * @param finishT the finish transaction to restore states after the transition + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition * @return {@code true} if window decoration was created, {@code false} otherwise */ - boolean createWindowDecoration( + boolean onTaskOpening( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, @@ -54,7 +53,7 @@ public interface WindowDecorViewModel { /** * Notifies a task info update on the given task, with the window decoration created previously - * for this task by {@link #createWindowDecoration}. + * for this task by {@link #onTaskOpening}. * * @param taskInfo the new task info of the task */ @@ -62,13 +61,29 @@ public interface WindowDecorViewModel { /** * Notifies a transition is about to start about the given task to give the window decoration a - * chance to prepare for this transition. + * chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates + * a window decoration if one does not exist but is required. + * + * @param taskInfo the initial task info of the task + * @param taskSurface the surface of the task + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition + */ + void onTaskChanging( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Notifies that the given task is about to close to give the window decoration a chance to + * prepare for this transition. * - * @param startT the start transaction to be applied before the transition - * @param finishT the finish transaction to restore states after the transition - * @return {@code true} if window decoration exists, {@code false} otherwise + * @param taskInfo the initial task info of the task + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition */ - boolean setupWindowDecorationForTransition( + void onTaskClosing( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT); @@ -77,7 +92,6 @@ public interface WindowDecorViewModel { * Destroys the window decoration of the give task. * * @param taskInfo the info of the task - * @return {@code true} if window decoration was destroyed, {@code false} otherwise */ - boolean destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); -} + void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 7068a84c3056..48415d47304c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -103,7 +103,7 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mWindowDecorViewModel).createWindowDecoration( + verify(mWindowDecorViewModel).onTaskOpening( change.getTaskInfo(), change.getLeash(), startT, finishT); } @@ -120,7 +120,7 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mWindowDecorViewModel).setupWindowDecorationForTransition( + verify(mWindowDecorViewModel).onTaskClosing( change.getTaskInfo(), startT, finishT); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java new file mode 100644 index 000000000000..8b134ed1dfe4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; +import android.view.Display; +import android.view.InputChannel; +import android.view.InputMonitor; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.rule.GrantPermissionRule; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeController; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** Tests of {@link CaptionWindowDecorViewModel} */ +@SmallTest +public class CaptionWindowDecorViewModelTests extends ShellTestCase { + @Mock private CaptionWindowDecoration mCaptionWindowDecoration; + + @Mock private CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; + + @Mock private Handler mMainHandler; + + @Mock private Choreographer mMainChoreographer; + + @Mock private ShellTaskOrganizer mTaskOrganizer; + + @Mock private DisplayController mDisplayController; + + @Mock private SyncTransactionQueue mSyncQueue; + + @Mock private DesktopModeController mDesktopModeController; + + @Mock private InputMonitor mInputMonitor; + + @Mock private InputChannel mInputChannel; + + @Mock private CaptionWindowDecorViewModel.EventReceiverFactory mEventReceiverFactory; + + @Mock private CaptionWindowDecorViewModel.EventReceiver mEventReceiver; + + @Mock private InputManager mInputManager; + + private final List<InputManager> mMockInputManagers = new ArrayList<>(); + + private CaptionWindowDecorViewModel mCaptionWindowDecorViewModel; + + @Before + public void setUp() { + mMockInputManagers.add(mInputManager); + + mCaptionWindowDecorViewModel = + new CaptionWindowDecorViewModel( + mContext, + mMainHandler, + mMainChoreographer, + mTaskOrganizer, + mDisplayController, + mSyncQueue, + mDesktopModeController, + mCaptionWindowDecorFactory, + new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class))); + mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory); + + doReturn(mCaptionWindowDecoration) + .when(mCaptionWindowDecorFactory) + .create(any(), any(), any(), any(), any(), any(), any(), any()); + + when(mInputManager.monitorGestureInput(any(), anyInt())).thenReturn(mInputMonitor); + when(mEventReceiverFactory.create(any(), any())).thenReturn(mEventReceiver); + when(mInputMonitor.getInputChannel()).thenReturn(mInputChannel); + } + + @Test + public void testDeleteCaptionOnChangeTransitionWhenNecessary() throws Exception { + Looper.prepare(); + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, WINDOWING_MODE_FREEFORM); + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + GrantPermissionRule.grant(android.Manifest.permission.MONITOR_INPUT); + + mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); + verify(mCaptionWindowDecorFactory) + .create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + verify(mCaptionWindowDecoration).close(); + } + + @Test + public void testCreateCaptionOnChangeTransitionWhenNecessary() throws Exception { + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, WINDOWING_MODE_UNDEFINED); + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + + verify(mCaptionWindowDecorFactory, never()) + .create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + + verify(mCaptionWindowDecorFactory) + .create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + } + + private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { + ActivityManager.RunningTaskInfo taskInfo = + new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true) + .build(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + return taskInfo; + } + + private static class MockObjectSupplier<T> implements Supplier<T> { + private final List<T> mObjects; + private final Supplier<T> mDefaultSupplier; + private int mNumOfCalls = 0; + + private MockObjectSupplier(List<T> objects, Supplier<T> defaultSupplier) { + mObjects = objects; + mDefaultSupplier = defaultSupplier; + } + + @Override + public T get() { + final T mock = + mNumOfCalls < mObjects.size() ? mObjects.get(mNumOfCalls) + : mDefaultSupplier.get(); + ++mNumOfCalls; + return mock; + } + } +} diff --git a/media/java/android/media/Image.java b/media/java/android/media/Image.java index 8a03afb77942..d6fe68253be6 100644 --- a/media/java/android/media/Image.java +++ b/media/java/android/media/Image.java @@ -86,8 +86,10 @@ public abstract class Image implements AutoCloseable { * * <p> * The format is one of the values from - * {@link android.graphics.ImageFormat ImageFormat}. The mapping between the - * formats and the planes is as follows: + * {@link android.graphics.ImageFormat ImageFormat}, + * {@link android.graphics.PixelFormat PixelFormat}, or + * {@link android.hardware.HardwareBuffer HardwareBuffer}. The mapping between the + * formats and the planes is as follows (any formats not listed will have 1 plane): * </p> * * <table> @@ -171,15 +173,18 @@ public abstract class Image implements AutoCloseable { * </tr> * <tr> * <td>{@link android.graphics.ImageFormat#YCBCR_P010 YCBCR_P010}</td> - * <td>1</td> + * <td>3</td> * <td>P010 is a 4:2:0 YCbCr semiplanar format comprised of a WxH Y plane - * followed by a Wx(H/2) CbCr plane. Each sample is represented by a 16-bit - * little-endian value, with the lower 6 bits set to zero. + * followed by a Wx(H/2) Cb and Cr planes. Each sample is represented by a 16-bit + * little-endian value, with the lower 6 bits set to zero. Since this is guaranteed to be + * a semi-planar format, the Cb plane can also be treated as an interleaved Cb/Cr plane. * </td> * </tr> * </table> * * @see android.graphics.ImageFormat + * @see android.graphics.PixelFormat + * @see android.hardware.HardwareBuffer */ public abstract int getFormat(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index af05078b4f3a..73bb5eb46f33 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -34,6 +34,8 @@ import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.UserManager; +import android.text.TextUtils; +import android.util.EventLog; import android.util.Log; import java.util.Arrays; @@ -96,6 +98,22 @@ public class InstallStart extends Activity { mAbortInstall = true; } } + + final String installerPackageNameFromIntent = getIntent().getStringExtra( + Intent.EXTRA_INSTALLER_PACKAGE_NAME); + if (installerPackageNameFromIntent != null) { + final String callingPkgName = getLaunchedFromPackage(); + if (!TextUtils.equals(installerPackageNameFromIntent, callingPkgName) + && mPackageManager.checkPermission(Manifest.permission.INSTALL_PACKAGES, + callingPkgName) != PackageManager.PERMISSION_GRANTED) { + Log.e(LOG_TAG, "The given installer package name " + installerPackageNameFromIntent + + " is invalid. Remove it."); + EventLog.writeEvent(0x534e4554, "236687884", getLaunchedFromUid(), + "Invalid EXTRA_INSTALLER_PACKAGE_NAME"); + getIntent().removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME); + } + } + if (mAbortInstall) { setResult(RESULT_CANCELED); finish(); diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml index f868c694cef9..e66e3a175336 100644 --- a/packages/SettingsLib/res/values-vi/strings.xml +++ b/packages/SettingsLib/res/values-vi/strings.xml @@ -278,7 +278,7 @@ <string name="mock_location_app_set" msgid="4706722469342913843">"Ứng dụng vị trí mô phỏng: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="debug_networking_category" msgid="6829757985772659599">"Mạng"</string> <string name="wifi_display_certification" msgid="1805579519992520381">"Chứng nhận hiển thị không dây"</string> - <string name="wifi_verbose_logging" msgid="1785910450009679371">"Bật ghi nhật ký chi tiết Wi‑Fi"</string> + <string name="wifi_verbose_logging" msgid="1785910450009679371">"Bật tính năng ghi nhật ký chi tiết Wi‑Fi"</string> <string name="wifi_scan_throttling" msgid="2985624788509913617">"Hạn chế quét tìm Wi-Fi"</string> <string name="wifi_non_persistent_mac_randomization" msgid="7482769677894247316">"Tạo địa chỉ MAC ngẫu nhiên, không cố định mỗi khi kết nối Wi-Fi"</string> <string name="mobile_data_always_on" msgid="8275958101875563572">"Dữ liệu di động luôn hoạt động"</string> @@ -290,7 +290,7 @@ <string name="bluetooth_select_avrcp_version_dialog_title" msgid="7846922290083709633">"Chọn phiên bản Bluetooth AVRCP"</string> <string name="bluetooth_select_map_version_string" msgid="526308145174175327">"Phiên bản Bluetooth MAP"</string> <string name="bluetooth_select_map_version_dialog_title" msgid="7085934373987428460">"Chọn phiên bản Bluetooth MAP"</string> - <string name="bluetooth_select_a2dp_codec_type" msgid="952001408455456494">"Codec âm thanh Bluetooth"</string> + <string name="bluetooth_select_a2dp_codec_type" msgid="952001408455456494">"Bộ mã hoá và giải mã âm thanh qua Bluetooth"</string> <string name="bluetooth_select_a2dp_codec_type_dialog_title" msgid="7510542404227225545">"Kích hoạt chế độ chọn codec\nâm thanh Bluetooth"</string> <string name="bluetooth_select_a2dp_codec_sample_rate" msgid="1638623076480928191">"Tốc độ lấy mẫu âm thanh Bluetooth"</string> <string name="bluetooth_select_a2dp_codec_sample_rate_dialog_title" msgid="5876305103137067798">"Kích hoạt chế độ chọn codec\nâm thanh Bluetooth: Tần số lấy mẫu"</string> @@ -384,7 +384,7 @@ <string name="window_blurs" msgid="6831008984828425106">"Cho phép làm mờ cửa sổ"</string> <string name="force_msaa" msgid="4081288296137775550">"Bắt buộc 4x MSAA"</string> <string name="force_msaa_summary" msgid="9070437493586769500">"Bật 4x MSAA trong ứng dụng OpenGL ES 2.0"</string> - <string name="show_non_rect_clip" msgid="7499758654867881817">"Gỡ lỗi hoạt động của clip không phải là hình chữ nhật"</string> + <string name="show_non_rect_clip" msgid="7499758654867881817">"Gỡ lỗi hoạt động của đoạn không phải hình chữ nhật"</string> <string name="track_frame_time" msgid="522674651937771106">"Kết xuất HWUI cấu hình"</string> <string name="enable_gpu_debug_layers" msgid="4986675516188740397">"Bật lớp gỡ lỗi GPU"</string> <string name="enable_gpu_debug_layers_summary" msgid="4921521407377170481">"Cho phép tải lớp gỡ lỗi GPU cho ứng dụng gỡ lỗi"</string> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 949bbfbabf0b..d1ac7d00c1cf 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1125,7 +1125,9 @@ <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration --> <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string> <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited --> - <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging is paused</string> + <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging paused</string> + <!-- [CHAR_LIMIT=80] Label for battery charging future pause --> + <string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging to <xliff:g id="dock_defender_threshold">%2$s</xliff:g></string> <!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed --> <string name="battery_info_status_unknown">Unknown</string> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 3c6f18c76f83..e624441ca777 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -167,25 +167,14 @@ java_library { } android_library { - name: "SystemUI-tests", + name: "SystemUI-tests-base", manifest: "tests/AndroidManifest-base.xml", - additional_manifests: ["tests/AndroidManifest.xml"], - resource_dirs: [ "tests/res", "res-product", "res-keyguard", "res", ], - srcs: [ - "tests/src/**/*.kt", - "tests/src/**/*.java", - "src/**/*.kt", - "src/**/*.java", - "src/**/I*.aidl", - ":ReleaseJavaFiles", - ":SystemUI-tests-utils", - ], static_libs: [ "WifiTrackerLib", "SystemUIAnimationLib", @@ -224,9 +213,6 @@ android_library { "metrics-helper-lib", "hamcrest-library", "androidx.test.rules", - "androidx.test.uiautomator", - "mockito-target-extended-minus-junit4", - "androidx.test.ext.junit", "testables", "truth-prebuilt", "monet", @@ -236,6 +222,27 @@ android_library { "LowLightDreamLib", "motion_tool_lib", ], +} + +android_library { + name: "SystemUI-tests", + manifest: "tests/AndroidManifest-base.xml", + additional_manifests: ["tests/AndroidManifest.xml"], + srcs: [ + "tests/src/**/*.kt", + "tests/src/**/*.java", + "src/**/*.kt", + "src/**/*.java", + "src/**/I*.aidl", + ":ReleaseJavaFiles", + ":SystemUI-tests-utils", + ], + static_libs: [ + "SystemUI-tests-base", + "androidx.test.uiautomator", + "mockito-target-extended-minus-junit4", + "androidx.test.ext.junit", + ], libs: [ "android.test.runner", "android.test.base", @@ -249,6 +256,45 @@ android_library { plugins: ["dagger2-compiler"], } +android_app { + name: "SystemUIRobo-stub", + defaults: [ + "platform_app_defaults", + "SystemUI_app_defaults", + ], + manifest: "tests/AndroidManifest-base.xml", + static_libs: [ + "SystemUI-tests-base", + ], + aaptflags: [ + "--extra-packages", + "com.android.systemui", + ], + dont_merge_manifests: true, + platform_apis: true, + system_ext_specific: true, + certificate: "platform", + privileged: true, + resource_dirs: [], +} + +android_robolectric_test { + name: "SystemUiRoboTests", + srcs: [ + "tests/robolectric/src/**/*.kt", + "tests/robolectric/src/**/*.java", + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "truth-prebuilt", + ], + kotlincflags: ["-Xjvm-default=enable"], + instrumentation_for: "SystemUIRobo-stub", + java_resource_dirs: ["tests/robolectric/config"], +} + // Opt-out config for optimizing the SystemUI target using R8. // Disabled via `export SYSTEMUI_OPTIMIZE_JAVA=false`, or explicitly in Make via // `SYSTEMUI_OPTIMIZE_JAVA := false`. diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp index 5df79e1bee94..e6ac48ff5af8 100644 --- a/packages/SystemUI/animation/Android.bp +++ b/packages/SystemUI/animation/Android.bp @@ -34,10 +34,7 @@ android_library { "res", ], - static_libs: [ - "PluginCoreLib", - "androidx.core_core-animation-nodeps", - ], + static_libs: ["androidx.core_core-animation-nodeps"], manifest: "AndroidManifest.xml", kotlincflags: ["-Xjvm-default=all"], 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 fdfad2bc2fa1..54aa3516d867 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -75,7 +75,7 @@ constructor( */ interface Controller { /** The [ViewRootImpl] of this controller. */ - val viewRoot: ViewRootImpl + val viewRoot: ViewRootImpl? /** * The identity object of the source animated by this controller. This animator will ensure @@ -807,15 +807,17 @@ private class AnimatedDialog( * inversely, removed from the overlay when the source is moved back to its original position). */ private fun synchronizeNextDraw(then: () -> Unit) { - if (forceDisableSynchronization) { - // Don't synchronize when inside an automated test. + val controllerRootView = controller.viewRoot?.view + if (forceDisableSynchronization || controllerRootView == null) { + // Don't synchronize when inside an automated test or if the controller root view is + // detached. then() return } - ViewRootSync.synchronizeNextDraw(controller.viewRoot.view, decorView, then) + ViewRootSync.synchronizeNextDraw(controllerRootView, decorView, then) decorView.invalidate() - controller.viewRoot.view.invalidate() + controllerRootView.invalidate() } private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt index f9c6841f96b5..43bfa74119b3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt @@ -320,9 +320,7 @@ class RemoteTransitionAdapter { counterWallpaper.cleanUp(finishTransaction) // Release surface references now. This is apparently to free GPU // memory while doing quick operations (eg. during CTS). - for (i in info.changes.indices.reversed()) { - info.changes[i].leash.release() - } + info.releaseAllSurfaces() for (i in leashMap.size - 1 downTo 0) { leashMap.valueAt(i).release() } @@ -331,6 +329,7 @@ class RemoteTransitionAdapter { null /* wct */, finishTransaction ) + finishTransaction.close() } catch (e: RemoteException) { Log.e( "ActivityOptionsCompat", @@ -364,6 +363,9 @@ class RemoteTransitionAdapter { ) { // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, // ignore any incoming merges. + // Clean up stuff though cuz GC takes too long for benchmark tests. + t.close() + info.releaseAllSurfaces() } } } 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 ecee598afe4e..964ef8c88098 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -28,7 +28,7 @@ internal constructor( private val source: View, override val cuj: DialogCuj?, ) : DialogLaunchAnimator.Controller { - override val viewRoot: ViewRootImpl + override val viewRoot: ViewRootImpl? get() = source.viewRootImpl override val sourceIdentity: Any = source diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt index 50c3d7e1e76b..d6db574a34ae 100644 --- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt @@ -262,7 +262,7 @@ internal class ExpandableControllerImpl( private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller { return object : DialogLaunchAnimator.Controller { - override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl + override val viewRoot: ViewRootImpl? = composeViewRoot.viewRootImpl override val sourceIdentity: Any = this@ExpandableControllerImpl override val cuj: DialogCuj? = cuj diff --git a/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt index e611e8bf0068..979e1a08b7d4 100644 --- a/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt +++ b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt @@ -38,12 +38,18 @@ import platform.test.screenshot.ScreenshotTestRule import platform.test.screenshot.getEmulatedDevicePathConfig /** A rule for Compose screenshot diff tests. */ -class ComposeScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule { +class ComposeScreenshotTestRule( + emulationSpec: DeviceEmulationSpec, + assetPathRelativeToBuildRoot: String +) : TestRule { private val colorsRule = MaterialYouColorsRule() private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) private val screenshotRule = ScreenshotTestRule( - SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) + SystemUIGoldenImagePathManager( + getEmulatedDevicePathConfig(emulationSpec), + assetPathRelativeToBuildRoot + ) ) private val composeRule = createAndroidComposeRule<ScreenshotActivity>() private val delegateRule = 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 869884474ffe..e1f21742bf93 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 @@ -20,7 +20,6 @@ import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater -import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.systemui.customization.R @@ -152,15 +151,9 @@ class DefaultClockController( view: AnimatableClockView, ) : DefaultClockFaceController(view) { override fun recomputePadding(targetRegion: Rect?) { - // We center the view within the targetRegion instead of within the parent - // view by computing the difference and adding that to the padding. - val parent = view.parent - val yDiff = - if (targetRegion != null && parent is View && parent.isLaidOut()) - targetRegion.centerY() - parent.height / 2f - else 0f + // Ignore Target Region until top padding fixed in aod val lp = view.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = (-0.5f * view.bottom + yDiff).toInt() + lp.topMargin = (-0.5f * view.bottom).toInt() view.setLayoutParams(lp) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..f490c5459d46 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,190 @@ +/* + * 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.shared.quickaffordance.data.content + +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine + +class FakeKeyguardQuickAffordanceProviderClient( + slots: List<KeyguardQuickAffordanceProviderClient.Slot> = + listOf( + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + capacity = 1, + ), + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + capacity = 1, + ), + ), + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance> = + listOf( + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_1, + name = AFFORDANCE_1, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_2, + name = AFFORDANCE_2, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_3, + name = AFFORDANCE_3, + iconResourceId = 0, + ), + ), + flags: List<KeyguardQuickAffordanceProviderClient.Flag> = + listOf( + KeyguardQuickAffordanceProviderClient.Flag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = true, + ) + ), +) : KeyguardQuickAffordanceProviderClient { + + private val slots = MutableStateFlow(slots) + private val affordances = MutableStateFlow(affordances) + private val flags = MutableStateFlow(flags) + + private val selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + + override suspend fun insertSelection(slotId: String, affordanceId: String) { + val slotCapacity = + querySlots().find { it.id == slotId }?.capacity + ?: error("Slot with ID \"$slotId\" not found!") + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + while (affordances.size + 1 > slotCapacity) { + affordances.removeAt(0) + } + affordances.remove(affordanceId) + affordances.add(affordanceId) + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return slots.value + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return flags.value + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return slots.asStateFlow() + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return flags.asStateFlow() + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return affordances.value + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return affordances.asStateFlow() + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return toSelectionList(selections.value, affordances.value) + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return combine(selections, affordances) { selections, affordances -> + toSelectionList(selections, affordances) + } + } + + override suspend fun deleteSelection(slotId: String, affordanceId: String) { + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + affordances.remove(affordanceId) + + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun deleteAllSelections(slotId: String) { + selections.value = selections.value.toMutableMap().apply { this[slotId] = emptyList() } + } + + override suspend fun getAffordanceIcon(iconResourceId: Int, tintColor: Int): Drawable { + return BitmapDrawable() + } + + fun setFlag( + name: String, + value: Boolean, + ) { + flags.value = + flags.value.toMutableList().apply { + removeIf { it.name == name } + add(KeyguardQuickAffordanceProviderClient.Flag(name = name, value = value)) + } + } + + fun setSlotCapacity(slotId: String, capacity: Int) { + slots.value = + slots.value.toMutableList().apply { + val index = indexOfFirst { it.id == slotId } + check(index != -1) { "Slot with ID \"$slotId\" doesn't exist!" } + set( + index, + KeyguardQuickAffordanceProviderClient.Slot(id = slotId, capacity = capacity) + ) + } + } + + fun addAffordance(affordance: KeyguardQuickAffordanceProviderClient.Affordance): Int { + affordances.value = affordances.value + listOf(affordance) + return affordances.value.size - 1 + } + + private fun toSelectionList( + selections: Map<String, List<String>>, + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance>, + ): List<KeyguardQuickAffordanceProviderClient.Selection> { + return selections + .map { (slotId, affordanceIds) -> + affordanceIds.map { affordanceId -> + val affordanceName = + affordances.find { it.id == affordanceId }?.name + ?: error("No affordance with ID of \"$affordanceId\"!") + KeyguardQuickAffordanceProviderClient.Selection( + slotId = slotId, + affordanceId = affordanceId, + affordanceName = affordanceName, + ) + } + } + .flatten() + } + + companion object { + const val AFFORDANCE_1 = "affordance_1" + const val AFFORDANCE_2 = "affordance_2" + const val AFFORDANCE_3 = "affordance_3" + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..3213b2e97ac9 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,479 @@ +/* + * 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.shared.quickaffordance.data.content + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.ContentObserver +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Client for using a content provider implementing the [Contract]. */ +interface KeyguardQuickAffordanceProviderClient { + + /** + * Selects an affordance with the given ID for a slot on the lock screen with the given ID. + * + * Note that the maximum number of selected affordances on this slot is automatically enforced. + * Selecting a slot that is already full (e.g. already has a number of selected affordances at + * its maximum capacity) will automatically remove the oldest selected affordance before adding + * the one passed in this call. Additionally, selecting an affordance that's already one of the + * selected affordances on the slot will move the selected affordance to the newest location in + * the slot. + */ + suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) + + /** Returns all available slots supported by the device. */ + suspend fun querySlots(): List<Slot> + + /** Returns the list of flags. */ + suspend fun queryFlags(): List<Flag> + + /** + * Returns [Flow] for observing the collection of slots. + * + * @see [querySlots] + */ + fun observeSlots(): Flow<List<Slot>> + + /** + * Returns [Flow] for observing the collection of flags. + * + * @see [queryFlags] + */ + fun observeFlags(): Flow<List<Flag>> + + /** + * Returns all available affordances supported by the device, regardless of current slot + * placement. + */ + suspend fun queryAffordances(): List<Affordance> + + /** + * Returns [Flow] for observing the collection of affordances. + * + * @see [queryAffordances] + */ + fun observeAffordances(): Flow<List<Affordance>> + + /** Returns the current slot-affordance selections. */ + suspend fun querySelections(): List<Selection> + + /** + * Returns [Flow] for observing the collection of selections. + * + * @see [querySelections] + */ + fun observeSelections(): Flow<List<Selection>> + + /** Unselects an affordance with the given ID from the slot with the given ID. */ + suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) + + /** Unselects all affordances from the slot with the given ID. */ + suspend fun deleteAllSelections( + slotId: String, + ) + + /** Returns a [Drawable] with the given ID, loaded from the system UI package. */ + suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int = Color.WHITE, + ): Drawable + + /** Models a slot. A position that quick affordances can be positioned in. */ + data class Slot( + /** Unique ID of the slot. */ + val id: String, + /** + * The maximum number of quick affordances that are allowed to be positioned in this slot. + */ + val capacity: Int, + ) + + /** + * Models a quick affordance. An action that can be selected by the user to appear in one or + * more slots on the lock screen. + */ + data class Affordance( + /** Unique ID of the quick affordance. */ + val id: String, + /** User-facing label for this affordance. */ + val name: String, + /** + * Resource ID for the user-facing icon for this affordance. This resource is hosted by the + * System UI process so it must be used with + * `PackageManager.getResourcesForApplication(String)`. + */ + val iconResourceId: Int, + /** + * Whether the affordance is enabled. Disabled affordances should be shown on the picker but + * should be rendered as "disabled". When tapped, the enablement properties should be used + * to populate UI that would explain to the user what to do in order to re-enable this + * affordance. + */ + val isEnabled: Boolean = true, + /** + * If the affordance is disabled, this is a set of instruction messages to be shown to the + * user when the disabled affordance is selected. The instructions should help the user + * figure out what to do in order to re-neable this affordance. + */ + val enablementInstructions: List<String>? = null, + /** + * If the affordance is disabled, this is a label for a button shown together with the set + * of instruction messages when the disabled affordance is selected. The button should help + * send the user to a flow that would help them achieve the instructions and re-enable this + * affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionText: String? = null, + /** + * If the affordance is disabled, this is a "component name" of the format + * `packageName/action` to be used as an `Intent` for `startActivity` when the action button + * (shown together with the set of instruction messages when the disabled affordance is + * selected) is clicked by the user. The button should help send the user to a flow that + * would help them achieve the instructions and re-enable this affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionComponentName: String? = null, + ) + + /** Models a selection of a quick affordance on a slot. */ + data class Selection( + /** The unique ID of the slot. */ + val slotId: String, + /** The unique ID of the quick affordance. */ + val affordanceId: String, + /** The user-visible label for the quick affordance. */ + val affordanceName: String, + ) + + /** Models a System UI flag. */ + data class Flag( + /** The name of the flag. */ + val name: String, + /** The value of the flag. */ + val value: Boolean, + ) +} + +class KeyguardQuickAffordanceProviderClientImpl( + private val context: Context, + private val backgroundDispatcher: CoroutineDispatcher, +) : KeyguardQuickAffordanceProviderClient { + + override suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.insert( + Contract.SelectionTable.URI, + ContentValues().apply { + put(Contract.SelectionTable.Columns.SLOT_ID, slotId) + put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId) + } + ) + } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SlotTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID) + val capacityColumnIndex = + cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY) + if (idColumnIndex == -1 || capacityColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Slot( + id = cursor.getString(idColumnIndex), + capacity = cursor.getInt(capacityColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.FlagsTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val nameColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.NAME) + val valueColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.VALUE) + if (nameColumnIndex == -1 || valueColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Flag( + name = cursor.getString(nameColumnIndex), + value = cursor.getInt(valueColumnIndex) == 1, + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return observeUri(Contract.SlotTable.URI).map { querySlots() } + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return observeUri(Contract.FlagsTable.URI).map { queryFlags() } + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.AffordanceTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID) + val nameColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME) + val iconColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON) + val isEnabledColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.IS_ENABLED) + val enablementInstructionsColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_INSTRUCTIONS + ) + val enablementActionTextColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_ACTION_TEXT + ) + val enablementComponentNameColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_COMPONENT_NAME + ) + if ( + idColumnIndex == -1 || + nameColumnIndex == -1 || + iconColumnIndex == -1 || + isEnabledColumnIndex == -1 || + enablementInstructionsColumnIndex == -1 || + enablementActionTextColumnIndex == -1 || + enablementComponentNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Affordance( + id = cursor.getString(idColumnIndex), + name = cursor.getString(nameColumnIndex), + iconResourceId = cursor.getInt(iconColumnIndex), + isEnabled = cursor.getInt(isEnabledColumnIndex) == 1, + enablementInstructions = + cursor + .getString(enablementInstructionsColumnIndex) + ?.split( + Contract.AffordanceTable + .ENABLEMENT_INSTRUCTIONS_DELIMITER + ), + enablementActionText = + cursor.getString(enablementActionTextColumnIndex), + enablementActionComponentName = + cursor.getString(enablementComponentNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return observeUri(Contract.AffordanceTable.URI).map { queryAffordances() } + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SelectionTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val slotIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID) + val affordanceNameColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_NAME) + if ( + slotIdColumnIndex == -1 || + affordanceIdColumnIndex == -1 || + affordanceNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Selection( + slotId = cursor.getString(slotIdColumnIndex), + affordanceId = cursor.getString(affordanceIdColumnIndex), + affordanceName = cursor.getString(affordanceNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return observeUri(Contract.SelectionTable.URI).map { querySelections() } + } + + override suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" + + " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?", + arrayOf( + slotId, + affordanceId, + ), + ) + } + } + + override suspend fun deleteAllSelections( + slotId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + Contract.SelectionTable.Columns.SLOT_ID, + arrayOf( + slotId, + ), + ) + } + } + + @SuppressLint("UseCompatLoadingForDrawables") + override suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int, + ): Drawable { + return withContext(backgroundDispatcher) { + context.packageManager + .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + .getDrawable(iconResourceId, context.theme) + .apply { setTint(tintColor) } + } + } + + private fun observeUri( + uri: Uri, + ): Flow<Unit> { + return callbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + context.contentResolver.registerContentObserver( + uri, + /* notifyForDescendants= */ true, + observer, + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + } + + companion object { + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt index 98d8d3eb9a4a..17be74b08690 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.shared.keyguard.data.content +package com.android.systemui.shared.quickaffordance.data.content import android.content.ContentResolver import android.net.Uri diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt index 2dc7a280e423..2dc7a280e423 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 0abbb1e5bd84..b75c5c722403 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -753,7 +753,7 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +-packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryStateNotifierTest.kt diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index 1282d771579e..f96644fbd667 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -23,14 +23,11 @@ -keep class ** extends androidx.preference.PreferenceFragment -keep class com.android.systemui.tuner.* -# The plugins and animation subpackages both act as shared libraries that might be referenced in +# The plugins subpackage acts as a shared library that might be referenced in # dynamically-loaded plugin APKs. -keep class com.android.systemui.plugins.** { *; } --keep class !com.android.systemui.animation.R$**,com.android.systemui.animation.** { - *; -} -keep class com.android.systemui.fragments.FragmentService$FragmentCreator { *; } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index b49afeef09f3..218c5cc9b7fe 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -35,6 +35,7 @@ android:visibility="invisible" /> <FrameLayout android:id="@+id/lockscreen_clock_view_large" + android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" diff --git a/packages/SystemUI/res-keyguard/values-h650dp/dimens.xml b/packages/SystemUI/res-keyguard/values-h650dp/dimens.xml index 669f8fb642de..e5e17b7d1554 100644 --- a/packages/SystemUI/res-keyguard/values-h650dp/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-h650dp/dimens.xml @@ -17,4 +17,7 @@ <resources> <dimen name="widget_big_font_size">54dp</dimen> + + <!-- Margin above the ambient indication container --> + <dimen name="ambient_indication_container_margin_top">10dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values-nl/strings.xml b/packages/SystemUI/res-keyguard/values-nl/strings.xml index 579824a2a734..57e5f8a49535 100644 --- a/packages/SystemUI/res-keyguard/values-nl/strings.xml +++ b/packages/SystemUI/res-keyguard/values-nl/strings.xml @@ -74,7 +74,7 @@ <string name="kg_password_pin_failed" msgid="5136259126330604009">"Bewerking met pincode voor simkaart is mislukt."</string> <string name="kg_password_puk_failed" msgid="6778867411556937118">"Bewerking met pukcode voor simkaart is mislukt."</string> <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Invoermethode wijzigen"</string> - <string name="airplane_mode" msgid="2528005343938497866">"Vliegtuigmodus"</string> + <string name="airplane_mode" msgid="2528005343938497866">"Vliegtuigmodus"</string> <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon vereist nadat het apparaat opnieuw is opgestart"</string> <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pincode vereist nadat het apparaat opnieuw is opgestart"</string> <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wachtwoord vereist nadat het apparaat opnieuw is opgestart"</string> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 3861d983b309..c5ffdc0051da 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -41,6 +41,9 @@ <!-- Minimum bottom margin under the security view --> <dimen name="keyguard_security_view_bottom_margin">60dp</dimen> + <!-- Margin above the ambient indication container --> + <dimen name="ambient_indication_container_margin_top">0dp</dimen> + <dimen name="keyguard_eca_top_margin">18dp</dimen> <dimen name="keyguard_eca_bottom_margin">12dp</dimen> diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index a129fb650ba6..da485a99c29b 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -53,7 +53,7 @@ <string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string> <!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited. --> - <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging is paused to protect battery</string> + <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging paused to protect battery</string> <!-- On the keyguard screen, when pattern lock is disabled, only tell them to press menu to unlock. This is shown in small font at the bottom. --> <string name="keyguard_instructions_when_pattern_disabled">Press Menu to unlock.</string> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index 2d67d95ab17e..efcb6f3435b9 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -14,25 +14,32 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.shared.shadow.DoubleShadowTextClock +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/time_view" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_clockFontFamily" - android:textColor="@android:color/white" - android:format12Hour="@string/dream_time_complication_12_hr_time_format" - android:format24Hour="@string/dream_time_complication_24_hr_time_format" - android:fontFeatureSettings="pnum, lnum" - android:letterSpacing="0.02" - android:textSize="@dimen/dream_overlay_complication_clock_time_text_size" - app:keyShadowBlur="@dimen/dream_overlay_clock_key_text_shadow_radius" - app:keyShadowOffsetX="@dimen/dream_overlay_clock_key_text_shadow_dx" - app:keyShadowOffsetY="@dimen/dream_overlay_clock_key_text_shadow_dy" - app:keyShadowAlpha="0.3" - app:ambientShadowBlur="@dimen/dream_overlay_clock_ambient_text_shadow_radius" - app:ambientShadowOffsetX="@dimen/dream_overlay_clock_ambient_text_shadow_dx" - app:ambientShadowOffsetY="@dimen/dream_overlay_clock_ambient_text_shadow_dy" - app:ambientShadowAlpha="0.3" -/> + android:layout_height="wrap_content"> + + <com.android.systemui.shared.shadow.DoubleShadowTextClock + android:id="@+id/time_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_clockFontFamily" + android:textColor="@android:color/white" + android:format12Hour="@string/dream_time_complication_12_hr_time_format" + android:format24Hour="@string/dream_time_complication_24_hr_time_format" + android:fontFeatureSettings="pnum, lnum" + android:letterSpacing="0.02" + android:textSize="@dimen/dream_overlay_complication_clock_time_text_size" + android:translationY="@dimen/dream_overlay_complication_clock_time_translation_y" + app:keyShadowBlur="@dimen/dream_overlay_clock_key_text_shadow_radius" + app:keyShadowOffsetX="@dimen/dream_overlay_clock_key_text_shadow_dx" + app:keyShadowOffsetY="@dimen/dream_overlay_clock_key_text_shadow_dy" + app:keyShadowAlpha="0.3" + app:ambientShadowBlur="@dimen/dream_overlay_clock_ambient_text_shadow_radius" + app:ambientShadowOffsetX="@dimen/dream_overlay_clock_ambient_text_shadow_dx" + app:ambientShadowOffsetY="@dimen/dream_overlay_clock_ambient_text_shadow_dy" + app:ambientShadowAlpha="0.3" + /> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml index 4f0a78e9c35d..de96e9765668 100644 --- a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml +++ b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml @@ -14,16 +14,21 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<ImageView +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/home_controls_chip" - android:layout_height="@dimen/keyguard_affordance_fixed_height" - android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:layout_gravity="bottom|start" - android:scaleType="center" - android:tint="?android:attr/textColorPrimary" - android:src="@drawable/controls_icon" - android:background="@drawable/keyguard_bottom_affordance_bg" - android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset" - android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" - android:contentDescription="@string/quick_controls_title" /> + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingVertical="@dimen/dream_overlay_complication_home_controls_padding"> + + <ImageView + android:id="@+id/home_controls_chip" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="bottom|start" + android:scaleType="center" + android:tint="?android:attr/textColorPrimary" + android:src="@drawable/controls_icon" + android:background="@drawable/keyguard_bottom_affordance_bg" + android:contentDescription="@string/quick_controls_title" /> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml index 79ba7ead1ec3..aa655e6b3564 100644 --- a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml +++ b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml @@ -41,7 +41,7 @@ android:layout_width="@dimen/qs_media_app_icon_size" android:layout_height="@dimen/qs_media_app_icon_size" android:layout_marginStart="@dimen/qs_media_padding" - android:layout_marginTop="@dimen/qs_media_padding" + android:layout_marginTop="@dimen/qs_media_rec_icon_top_margin" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/packages/SystemUI/res/values-h700dp/dimens.xml b/packages/SystemUI/res/values-h700dp/dimens.xml new file mode 100644 index 000000000000..055308f17776 --- /dev/null +++ b/packages/SystemUI/res/values-h700dp/dimens.xml @@ -0,0 +1,20 @@ +<!-- + ~ 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> + <!-- Margin above the ambient indication container --> + <dimen name="ambient_indication_container_margin_top">15dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml index 8efd6f0b7995..3a71994e07e2 100644 --- a/packages/SystemUI/res/values-h800dp/dimens.xml +++ b/packages/SystemUI/res/values-h800dp/dimens.xml @@ -17,4 +17,7 @@ <resources> <!-- With the large clock, move up slightly from the center --> <dimen name="keyguard_large_clock_top_margin">-112dp</dimen> + + <!-- Margin above the ambient indication container --> + <dimen name="ambient_indication_container_margin_top">20dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml index 868c003d99a5..3fc59e38ec6c 100644 --- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml @@ -35,6 +35,11 @@ not appear immediately after user swipes to the side --> <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_album_side_margin">16dp</dimen> + <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen> <!-- Roughly the same distance as media on LS to media on QS. We will translate by this value diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 738981db6823..e849b1f06ffd 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1051,6 +1051,7 @@ <dimen name="qs_media_session_collapsed_guideline">144dp</dimen> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> + <dimen name="qs_media_rec_icon_top_margin">16dp</dimen> <dimen name="qs_media_rec_album_size">88dp</dimen> <dimen name="qs_media_rec_album_side_margin">16dp</dimen> <dimen name="qs_media_rec_album_bottom_margin">8dp</dimen> @@ -1500,13 +1501,15 @@ <dimen name="dream_overlay_status_bar_extra_margin">8dp</dimen> <!-- Dream overlay complications related dimensions --> - <dimen name="dream_overlay_complication_clock_time_text_size">86sp</dimen> + <dimen name="dream_overlay_complication_clock_time_text_size">86dp</dimen> + <dimen name="dream_overlay_complication_clock_time_translation_y">28dp</dimen> <dimen name="dream_overlay_complication_home_controls_padding">28dp</dimen> <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen> <dimen name="dream_overlay_complication_preview_text_size">36sp</dimen> <dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen> <dimen name="dream_overlay_complication_shadow_padding">2dp</dimen> <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> + <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e166bb9708bf..b39f49fbca8e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -200,6 +200,8 @@ <!-- Informs the user that a screenshot is being saved. [CHAR LIMIT=50] --> <string name="screenshot_saving_title">Saving screenshot\u2026</string> + <!-- Informs the user that a screenshot is being saved. [CHAR LIMIT=50] --> + <string name="screenshot_saving_work_profile_title">Saving screenshot to work profile\u2026</string> <!-- Notification title displayed when a screenshot is saved to the Gallery. [CHAR LIMIT=50] --> <string name="screenshot_saved_title">Screenshot saved</string> <!-- Notification title displayed when we fail to take a screenshot. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt index 49cc48321d77..e032bb9b7e30 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt @@ -34,13 +34,19 @@ import platform.test.screenshot.* /** * A rule that allows to run a screenshot diff test on a view that is hosted in another activity. */ -class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule { +class ExternalViewScreenshotTestRule( + emulationSpec: DeviceEmulationSpec, + assetPathRelativeToBuildRoot: String +) : TestRule { private val colorsRule = MaterialYouColorsRule() private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) private val screenshotRule = ScreenshotTestRule( - SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) + SystemUIGoldenImagePathManager( + getEmulatedDevicePathConfig(emulationSpec), + assetPathRelativeToBuildRoot + ) ) private val delegateRule = RuleChain.outerRule(colorsRule).around(deviceEmulationRule).around(screenshotRule) diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/SystemUIGoldenImagePathManager.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/SystemUIGoldenImagePathManager.kt index fafc7744f439..72d8c5a09852 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/SystemUIGoldenImagePathManager.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/SystemUIGoldenImagePathManager.kt @@ -23,11 +23,11 @@ import platform.test.screenshot.PathConfig /** A [GoldenImagePathManager] that should be used for all SystemUI screenshot tests. */ class SystemUIGoldenImagePathManager( pathConfig: PathConfig, - override val assetsPathRelativeToRepo: String = "tests/screenshot/assets" + assetsPathRelativeToBuildRoot: String ) : GoldenImagePathManager( appContext = InstrumentationRegistry.getInstrumentation().context, - assetsPathRelativeToRepo = assetsPathRelativeToRepo, + assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot, deviceLocalPath = InstrumentationRegistry.getInstrumentation() .targetContext diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt index 0b0595f4405f..738b37c9eea4 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -41,13 +41,17 @@ import platform.test.screenshot.matchers.BitmapMatcher /** A rule for View screenshot diff unit tests. */ class ViewScreenshotTestRule( emulationSpec: DeviceEmulationSpec, - private val matcher: BitmapMatcher = UnitTestBitmapMatcher + private val matcher: BitmapMatcher = UnitTestBitmapMatcher, + assetsPathRelativeToBuildRoot: String ) : TestRule { private val colorsRule = MaterialYouColorsRule() private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) private val screenshotRule = ScreenshotTestRule( - SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) + SystemUIGoldenImagePathManager( + getEmulatedDevicePathConfig(emulationSpec), + assetsPathRelativeToBuildRoot + ) ) private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) private val delegateRule = diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index f6c75a2d2752..c9ea79432360 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -39,6 +39,7 @@ public class PreviewPositionHelper { private boolean mIsOrientationChanged; private SplitBounds mSplitBounds; private int mDesiredStagePosition; + private boolean mTaskbarInApp; public Matrix getMatrix() { return mMatrix; @@ -57,6 +58,10 @@ public class PreviewPositionHelper { mDesiredStagePosition = desiredStagePosition; } + public void setTaskbarInApp(boolean taskbarInApp) { + mTaskbarInApp = taskbarInApp; + } + /** * Updates the matrix based on the provided parameters */ @@ -83,8 +88,18 @@ public class PreviewPositionHelper { ? mSplitBounds.topTaskPercent : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent)); // Scale portrait height to that of (actual screen - taskbar inset) - fullscreenTaskHeight = (screenHeightPx - taskbarSize) * taskPercent; - canvasScreenRatio = canvasHeight / fullscreenTaskHeight; + fullscreenTaskHeight = (screenHeightPx) * taskPercent; + if (mTaskbarInApp) { + canvasScreenRatio = canvasHeight / fullscreenTaskHeight; + } else { + if (mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) { + // Top app isn't cropped at all by taskbar + canvasScreenRatio = 0; + } else { + // Same as fullscreen ratio + canvasScreenRatio = (float) canvasWidth / screenWidthPx; + } + } } else { // For landscape, scale the width taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt index 3748eba47be5..19d0a3d6bf32 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt @@ -71,7 +71,7 @@ class DoubleShadowIconDrawable( mKeyShadowInfo.offsetY, mKeyShadowInfo.alpha ) - val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DARKEN) + val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP) renderNode.setRenderEffect(blend) return renderNode } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java index 93c807352521..1b0dacc327c1 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java @@ -166,15 +166,14 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner counterLauncher.cleanUp(finishTransaction); counterWallpaper.cleanUp(finishTransaction); // Release surface references now. This is apparently to free GPU memory - // while doing quick operations (eg. during CTS). - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - info.getChanges().get(i).getLeash().release(); - } + // before GC would. + info.releaseAllSurfaces(); // Don't release here since launcher might still be using them. Instead // let launcher release them (eg. via RemoteAnimationTargets) leashMap.clear(); try { finishCallback.onTransitionFinished(null /* wct */, finishTransaction); + finishTransaction.close(); } catch (RemoteException e) { Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" + " finished callback", e); @@ -203,10 +202,13 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner synchronized (mFinishRunnables) { finishRunnable = mFinishRunnables.remove(mergeTarget); } + // Since we're not actually animating, release native memory now + t.close(); + info.releaseAllSurfaces(); if (finishRunnable == null) return; onAnimationCancelled(false /* isKeyguardOccluded */); finishRunnable.run(); } }; } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index d4d3d2579b10..b7e2494ab839 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -126,15 +126,18 @@ public class RemoteTransitionCompat { public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishedCallback) { - if (!mergeTarget.equals(mToken)) return; - if (!mRecentsSession.merge(info, t, recents)) return; - try { - finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); - } catch (RemoteException e) { - Log.e(TAG, "Error merging transition.", e); + if (mergeTarget.equals(mToken) && mRecentsSession.merge(info, t, recents)) { + try { + finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); + } catch (RemoteException e) { + Log.e(TAG, "Error merging transition.", e); + } + // commit taskAppeared after merge transition finished. + mRecentsSession.commitTasksAppearedIfNeeded(recents); + } else { + t.close(); + info.releaseAllSurfaces(); } - // commit taskAppeared after merge transition finished. - mRecentsSession.commitTasksAppearedIfNeeded(recents); } }; return new RemoteTransition(remote, appThread); @@ -248,6 +251,8 @@ public class RemoteTransitionCompat { } // In this case, we are "returning" to an already running app, so just consume // the merge and do nothing. + info.releaseAllSurfaces(); + t.close(); return true; } final int layer = mInfo.getChanges().size() * 3; @@ -264,6 +269,8 @@ public class RemoteTransitionCompat { t.setLayer(targets[i].leash, layer); } t.apply(); + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); mAppearedTargets = targets; return true; } @@ -380,9 +387,7 @@ public class RemoteTransitionCompat { } // Only release the non-local created surface references. The animator is responsible // for releasing the leashes created by local. - for (int i = 0; i < mInfo.getChanges().size(); ++i) { - mInfo.getChanges().get(i).getLeash().release(); - } + mInfo.releaseAllSurfaces(); // Reset all members. mWrapped = null; mFinishCB = null; diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java index 458d22efd206..a25b281f807c 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java @@ -25,7 +25,6 @@ import android.widget.Button; import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.widget.LockPatternUtils; -import com.android.settingslib.Utils; /** * This class implements a smart emergency button that updates itself based @@ -91,17 +90,6 @@ public class EmergencyButton extends Button { return super.onTouchEvent(event); } - /** - * Reload colors from resources. - **/ - public void reloadColors() { - int color = Utils.getColorAttrDefaultColor(getContext(), - com.android.internal.R.attr.textColorOnAccent); - setTextColor(color); - setBackground(getContext() - .getDrawable(com.android.systemui.R.drawable.kg_emergency_button_background)); - } - @Override public boolean performLongClick() { return super.performLongClick(); diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java index c5190e828e35..ea808eb19b90 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java @@ -135,7 +135,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { mPowerManager.userActivity(SystemClock.uptimeMillis(), true); } mActivityTaskManager.stopSystemLockTaskMode(); - mShadeController.collapsePanel(false); + mShadeController.collapseShade(false); if (mTelecomManager != null && mTelecomManager.isInCall()) { mTelecomManager.showInCallScreen(false); if (mEmergencyButtonCallback != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index 3e32cf5521ff..860c8e3a9f77 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -20,7 +20,6 @@ import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL; import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED; import static com.android.keyguard.KeyguardAbsKeyInputView.MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT; -import android.annotation.CallSuper; import android.content.res.ColorStateList; import android.os.AsyncTask; import android.os.CountDownTimer; @@ -117,13 +116,6 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey } } - @CallSuper - @Override - public void reloadColors() { - super.reloadColors(); - mMessageAreaController.reloadColors(); - } - @Override public boolean needsInput() { return false; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index faaba63938bf..2e9ad5868eba 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.annotation.CallSuper; import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -142,16 +141,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> public void showMessage(CharSequence message, ColorStateList colorState) { } - /** - * Reload colors from resources. - **/ - @CallSuper - public void reloadColors() { - if (mEmergencyButton != null) { - mEmergencyButton.reloadColors(); - } - } - public void startAppearAnimation() { if (TextUtils.isEmpty(mMessageAreaController.getMessage())) { mMessageAreaController.setMessage(getInitialMessageResId()); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt index 819768544b0c..e6283b86283b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt @@ -26,7 +26,6 @@ data class KeyguardFingerprintListenModel( val credentialAttempted: Boolean, val deviceInteractive: Boolean, val dreaming: Boolean, - val encryptedOrLockdown: Boolean, val fingerprintDisabled: Boolean, val fingerprintLockedOut: Boolean, val goingToSleep: Boolean, @@ -37,6 +36,7 @@ data class KeyguardFingerprintListenModel( val primaryUser: Boolean, val shouldListenSfpsState: Boolean, val shouldListenForFingerprintAssistant: Boolean, + val strongerAuthRequired: Boolean, val switchingUser: Boolean, val udfps: Boolean, val userDoesNotHaveTrust: Boolean diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index c29f632b88d3..6a9216218d07 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -116,13 +116,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> return mView.getText(); } - /** - * Reload colors from resources. - **/ - public void reloadColors() { - mView.reloadColor(); - } - /** Factory for creating {@link com.android.keyguard.KeyguardMessageAreaController}. */ public static class Factory { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 0025986c0e5c..195e8f92754d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.os.UserHandle; import android.text.Editable; @@ -39,7 +38,6 @@ import android.widget.TextView.OnEditorActionListener; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; -import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; @@ -95,18 +93,6 @@ public class KeyguardPasswordViewController } }; - @Override - public void reloadColors() { - super.reloadColors(); - int textColor = Utils.getColorAttr(mView.getContext(), - android.R.attr.textColorPrimary).getDefaultColor(); - mPasswordEntry.setTextColor(textColor); - mPasswordEntry.setHighlightColor(textColor); - mPasswordEntry.setBackgroundTintList(ColorStateList.valueOf(textColor)); - mPasswordEntry.setForegroundTintList(ColorStateList.valueOf(textColor)); - mSwitchImeButton.setImageTintList(ColorStateList.valueOf(textColor)); - } - protected KeyguardPasswordViewController(KeyguardPasswordView view, KeyguardUpdateMonitor keyguardUpdateMonitor, SecurityMode securityMode, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index cdbfb2492e27..571d2740773d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -35,7 +35,6 @@ import com.android.internal.widget.LockPatternView.Cell; import com.android.internal.widget.LockscreenCredential; import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; -import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; @@ -272,16 +271,6 @@ public class KeyguardPatternViewController } @Override - public void reloadColors() { - super.reloadColors(); - mMessageAreaController.reloadColors(); - int textColor = Utils.getColorAttr(mLockPatternView.getContext(), - android.R.attr.textColorSecondary).getDefaultColor(); - int errorColor = Utils.getColorError(mLockPatternView.getContext()).getDefaultColor(); - mLockPatternView.setColors(textColor, textColor, errorColor); - } - - @Override public void onPause() { super.onPause(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 7876f071fdf5..f51ac325c9c1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -70,12 +70,6 @@ public class KeyguardPinViewController } @Override - public void reloadColors() { - super.reloadColors(); - mView.reloadColors(); - } - - @Override public boolean startDisappearAnimation(Runnable finishRunnable) { return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 01be33e1e156..a72a484fb6f1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -363,16 +363,18 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard final boolean sfpsEnabled = getResources().getBoolean( R.bool.config_show_sidefps_hint_on_bouncer); final boolean fpsDetectionRunning = mUpdateMonitor.isFingerprintDetectionRunning(); - final boolean needsStrongAuth = mUpdateMonitor.userNeedsStrongAuth(); + final boolean isUnlockingWithFpAllowed = + mUpdateMonitor.isUnlockingWithFingerprintAllowed(); - boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning && !needsStrongAuth; + boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning + && isUnlockingWithFpAllowed; if (DEBUG) { Log.d(TAG, "sideFpsToShow=" + toShow + ", " + "mBouncerVisible=" + mBouncerVisible + ", " + "configEnabled=" + sfpsEnabled + ", " + "fpsDetectionRunning=" + fpsDetectionRunning + ", " - + "needsStrongAuth=" + needsStrongAuth); + + "isUnlockingWithFpAllowed=" + isUnlockingWithFpAllowed); } if (toShow) { mSideFpsController.get().show(SideFpsUiRequestSource.PRIMARY_BOUNCER); @@ -728,16 +730,20 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } private void reloadColors() { - mSecurityViewFlipperController.reloadColors(); + resetViewFlipper(); mView.reloadColors(); } /** Handles density or font scale changes. */ private void onDensityOrFontScaleChanged() { - mSecurityViewFlipperController.onDensityOrFontScaleChanged(); + resetViewFlipper(); + mView.onDensityOrFontScaleChanged(); + } + + private void resetViewFlipper() { + mSecurityViewFlipperController.clearViews(); mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode, mKeyguardSecurityCallback); - mView.onDensityOrFontScaleChanged(); } static class Factory { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java index 25afe11ac536..a5c8c7881e3b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java @@ -74,17 +74,8 @@ public class KeyguardSecurityViewFlipperController } } - /** - * Reload colors of ui elements upon theme change. - */ - public void reloadColors() { - for (KeyguardInputViewController<KeyguardInputView> child : mChildren) { - child.reloadColors(); - } - } - /** Handles density or font scale changes. */ - public void onDensityOrFontScaleChanged() { + public void clearViews() { mView.removeAllViews(); mChildren.clear(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 76f7d785071d..db4447399e13 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -133,12 +133,6 @@ public class KeyguardSimPinViewController } @Override - public void reloadColors() { - super.reloadColors(); - mView.reloadColors(); - } - - @Override protected void verifyPasswordAndUnlock() { String entry = mPasswordEntry.getText(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index 5995e859c786..e9405eb79901 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -121,12 +121,6 @@ public class KeyguardSimPukViewController } @Override - public void reloadColors() { - super.reloadColors(); - mView.reloadColors(); - } - - @Override protected void verifyPasswordAndUnlock() { mStateMachine.next(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 331497e4bf85..c11a5c17309c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -27,6 +27,8 @@ import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_P import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED; import static android.hardware.biometrics.BiometricConstants.LockoutMode; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; +import static android.hardware.biometrics.BiometricSourceType.FACE; +import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; @@ -229,7 +231,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Biometric authentication: Cancelling and waiting for the relevant biometric service to * send us the confirmation that cancellation has happened. */ - private static final int BIOMETRIC_STATE_CANCELLING = 2; + @VisibleForTesting + protected static final int BIOMETRIC_STATE_CANCELLING = 2; + + /** + * Biometric state: During cancelling we got another request to start listening, so when we + * receive the cancellation done signal, we should start listening again. + */ + @VisibleForTesting + protected static final int BIOMETRIC_STATE_CANCELLING_RESTARTING = 3; /** * Action indicating keyguard *can* start biometric authentiation. @@ -244,12 +254,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ private static final int BIOMETRIC_ACTION_UPDATE = 2; - /** - * Biometric state: During cancelling we got another request to start listening, so when we - * receive the cancellation done signal, we should start listening again. - */ - private static final int BIOMETRIC_STATE_CANCELLING_RESTARTING = 3; - @VisibleForTesting public static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1; public static final int BIOMETRIC_HELP_FACE_NOT_RECOGNIZED = -2; @@ -372,7 +376,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private KeyguardBypassController mKeyguardBypassController; private List<SubscriptionInfo> mSubscriptionInfo; - private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; + @VisibleForTesting + protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; private int mFaceRunningState = BIOMETRIC_STATE_STOPPED; private boolean mIsDreaming; private boolean mLogoutEnabled; @@ -746,8 +751,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void requestFaceAuthOnOccludingApp(boolean request) { mOccludingAppRequestingFace = request; - updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED); + int action = mOccludingAppRequestingFace ? BIOMETRIC_ACTION_UPDATE : BIOMETRIC_ACTION_STOP; + updateFaceListeningState(action, FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED); } /** @@ -806,7 +811,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab new BiometricAuthenticated(true, isStrongBiometric)); // Update/refresh trust state only if user can skip bouncer if (getUserCanSkipBouncer(userId)) { - mTrustManager.unlockedByBiometricForUser(userId, BiometricSourceType.FINGERPRINT); + mTrustManager.unlockedByBiometricForUser(userId, FINGERPRINT); } // Don't send cancel if authentication succeeds mFingerprintCancelSignal = null; @@ -816,7 +821,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricAuthenticated(userId, BiometricSourceType.FINGERPRINT, + cb.onBiometricAuthenticated(userId, FINGERPRINT, isStrongBiometric); } } @@ -849,7 +854,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + cb.onBiometricAuthFailed(FINGERPRINT); } } if (isUdfpsSupported()) { @@ -874,7 +879,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricAcquired(BiometricSourceType.FINGERPRINT, acquireInfo); + cb.onBiometricAcquired(FINGERPRINT, acquireInfo); } } } @@ -908,7 +913,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricHelp(msgId, helpString, BiometricSourceType.FINGERPRINT); + cb.onBiometricHelp(msgId, helpString, FINGERPRINT); } } } @@ -960,7 +965,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) { lockedOutStateChanged = !mFingerprintLockedOutPermanent; mFingerprintLockedOutPermanent = true; - mLogger.d("Fingerprint locked out - requiring strong auth"); + mLogger.d("Fingerprint permanently locked out - requiring stronger auth"); mLockPatternUtils.requireStrongAuth( STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, getCurrentUser()); } @@ -969,6 +974,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) { lockedOutStateChanged |= !mFingerprintLockedOut; mFingerprintLockedOut = true; + mLogger.d("Fingerprint temporarily locked out - requiring stronger auth"); if (isUdfpsEnrolled()) { updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } @@ -979,12 +985,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricError(msgId, errString, BiometricSourceType.FINGERPRINT); + cb.onBiometricError(msgId, errString, FINGERPRINT); } } if (lockedOutStateChanged) { - notifyLockedOutStateChanged(BiometricSourceType.FINGERPRINT); + notifyLockedOutStateChanged(FINGERPRINT); } } @@ -1012,7 +1018,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (changed) { - notifyLockedOutStateChanged(BiometricSourceType.FINGERPRINT); + notifyLockedOutStateChanged(FINGERPRINT); } } @@ -1035,7 +1041,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { cb.onBiometricRunningStateChanged(isFingerprintDetectionRunning(), - BiometricSourceType.FINGERPRINT); + FINGERPRINT); } } } @@ -1048,7 +1054,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab new BiometricAuthenticated(true, isStrongBiometric)); // Update/refresh trust state only if user can skip bouncer if (getUserCanSkipBouncer(userId)) { - mTrustManager.unlockedByBiometricForUser(userId, BiometricSourceType.FACE); + mTrustManager.unlockedByBiometricForUser(userId, FACE); } // Don't send cancel if authentication succeeds mFaceCancelSignal = null; @@ -1059,7 +1065,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { cb.onBiometricAuthenticated(userId, - BiometricSourceType.FACE, + FACE, isStrongBiometric); } } @@ -1081,7 +1087,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricAuthFailed(BiometricSourceType.FACE); + cb.onBiometricAuthFailed(FACE); } } handleFaceHelp(BIOMETRIC_HELP_FACE_NOT_RECOGNIZED, @@ -1094,7 +1100,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricAcquired(BiometricSourceType.FACE, acquireInfo); + cb.onBiometricAcquired(FACE, acquireInfo); } } } @@ -1129,7 +1135,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { - cb.onBiometricHelp(msgId, helpString, BiometricSourceType.FACE); + cb.onBiometricHelp(msgId, helpString, FACE); } } } @@ -1197,12 +1203,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { cb.onBiometricError(msgId, errString, - BiometricSourceType.FACE); + FACE); } } if (lockedOutStateChanged) { - notifyLockedOutStateChanged(BiometricSourceType.FACE); + notifyLockedOutStateChanged(FACE); } } @@ -1216,7 +1222,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET), getBiometricLockoutDelay()); if (changed) { - notifyLockedOutStateChanged(BiometricSourceType.FACE); + notifyLockedOutStateChanged(FACE); } } @@ -1239,7 +1245,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { cb.onBiometricRunningStateChanged(isFaceDetectionRunning(), - BiometricSourceType.FACE); + FACE); } } } @@ -1380,7 +1386,35 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) { - return mStrongAuthTracker.isUnlockingWithBiometricAllowed(isStrongBiometric); + // StrongAuthTracker#isUnlockingWithBiometricAllowed includes + // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent; + // however the strong auth tracker does not include the temporary lockout + // mFingerprintLockedOut. + return mStrongAuthTracker.isUnlockingWithBiometricAllowed(isStrongBiometric) + && !mFingerprintLockedOut; + } + + /** + * Whether fingerprint is allowed ot be used for unlocking based on the strongAuthTracker + * and temporary lockout state (tracked by FingerprintManager via error codes). + */ + public boolean isUnlockingWithFingerprintAllowed() { + return isUnlockingWithBiometricAllowed(FINGERPRINT); + } + + /** + * Whether the given biometric is allowed based on strongAuth & lockout states. + */ + public boolean isUnlockingWithBiometricAllowed( + @NonNull BiometricSourceType biometricSourceType) { + switch (biometricSourceType) { + case FINGERPRINT: + return isUnlockingWithBiometricAllowed(true); + case FACE: + return isUnlockingWithBiometricAllowed(false); + default: + return false; + } } public boolean isUserInLockdown(int userId) { @@ -1402,11 +1436,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return isEncrypted || isLockDown; } - public boolean userNeedsStrongAuth() { - return mStrongAuthTracker.getStrongAuthForUser(getCurrentUser()) - != LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; - } - private boolean containsFlag(int haystack, int needle) { return (haystack & needle) != 0; } @@ -1576,12 +1605,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } }; - private final FingerprintManager.FingerprintDetectionCallback mFingerprintDetectionCallback - = (sensorId, userId, isStrongBiometric) -> { - // Trigger the fingerprint success path so the bouncer can be shown - handleFingerprintAuthenticated(userId, isStrongBiometric); - }; - /** * Propagates a pointer down event to keyguard. */ @@ -2651,27 +2674,25 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && (!mKeyguardGoingAway || !mDeviceInteractive) && mIsPrimaryUser && biometricEnabledForUser; - - final boolean shouldListenBouncerState = !(mFingerprintLockedOut - && mPrimaryBouncerIsOrWillBeShowing && mCredentialAttempted); - - final boolean isEncryptedOrLockdownForUser = isEncryptedOrLockdown(user); + final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed(); + final boolean isSideFps = isSfpsSupported() && isSfpsEnrolled(); + final boolean shouldListenBouncerState = + !strongerAuthRequired || !mPrimaryBouncerIsOrWillBeShowing; final boolean shouldListenUdfpsState = !isUdfps || (!userCanSkipBouncer - && !isEncryptedOrLockdownForUser + && !strongerAuthRequired && userDoesNotHaveTrust); boolean shouldListenSideFpsState = true; - if (isSfpsSupported() && isSfpsEnrolled()) { + if (isSideFps) { shouldListenSideFpsState = mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true; } boolean shouldListen = shouldListenKeyguardState && shouldListenUserState - && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut() + && shouldListenBouncerState && shouldListenUdfpsState && shouldListenSideFpsState; - maybeLogListenerModelData( new KeyguardFingerprintListenModel( System.currentTimeMillis(), @@ -2683,7 +2704,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mCredentialAttempted, mDeviceInteractive, mIsDreaming, - isEncryptedOrLockdownForUser, fingerprintDisabledForUser, mFingerprintLockedOut, mGoingToSleep, @@ -2694,6 +2714,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mIsPrimaryUser, shouldListenSideFpsState, shouldListenForFingerprintAssistant, + strongerAuthRequired, mSwitchingUser, isUdfps, userDoesNotHaveTrust)); @@ -2721,10 +2742,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean isEncryptedOrTimedOut = containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_BOOT) || containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT); - - // TODO: always disallow when fp is already locked out? - final boolean fpLockedOut = mFingerprintLockedOut || mFingerprintLockedOutPermanent; - + final boolean fpLockedOut = isFingerprintLockedOut(); final boolean canBypass = mKeyguardBypassController != null && mKeyguardBypassController.canBypass(); // There's no reason to ask the HAL for authentication when the user can dismiss the @@ -2846,15 +2864,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Waiting for restart via handleFingerprintError(). return; } - mLogger.v("startListeningForFingerprint()"); if (unlockPossible) { mFingerprintCancelSignal = new CancellationSignal(); - if (isEncryptedOrLockdown(userId)) { - mFpm.detectFingerprint(mFingerprintCancelSignal, mFingerprintDetectionCallback, + if (!isUnlockingWithFingerprintAllowed()) { + mLogger.v("startListeningForFingerprint - detect"); + mFpm.detectFingerprint( + mFingerprintCancelSignal, + (sensorId, user, isStrongBiometric) -> { + mLogger.d("fingerprint detected"); + // Trigger the fingerprint success path so the bouncer can be shown + handleFingerprintAuthenticated(user, isStrongBiometric); + }, userId); } else { + mLogger.v("startListeningForFingerprint - authenticate"); mFpm.authenticate(null /* crypto */, mFingerprintCancelSignal, mFingerprintAuthenticationCallback, null /* handler */, FingerprintManager.SENSOR_ID_ANY, userId, 0 /* flags */); @@ -3071,11 +3096,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } + // Immediately stop previous biometric listening states. + // Resetting lockout states updates the biometric listening states. if (mFaceManager != null && !mFaceSensorProperties.isEmpty()) { + stopListeningForFace(FACE_AUTH_UPDATED_USER_SWITCHING); handleFaceLockoutReset(mFaceManager.getLockoutModeForUser( mFaceSensorProperties.get(0).sensorId, userId)); } if (mFpm != null && !mFingerprintSensorProperties.isEmpty()) { + stopListeningForFingerprint(); handleFingerprintLockoutReset(mFpm.getLockoutModeForUser( mFingerprintSensorProperties.get(0).sensorId, userId)); } @@ -3482,7 +3511,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @AnyThread public void setSwitchingUser(boolean switching) { mSwitchingUser = switching; - // Since this comes in on a binder thread, we need to post if first + // Since this comes in on a binder thread, we need to post it first mHandler.post(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_USER_SWITCHING)); } @@ -3574,8 +3603,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Assert.isMainThread(); mUserFingerprintAuthenticated.clear(); mUserFaceAuthenticated.clear(); - mTrustManager.clearAllBiometricRecognized(BiometricSourceType.FINGERPRINT, unlockedUser); - mTrustManager.clearAllBiometricRecognized(BiometricSourceType.FACE, unlockedUser); + mTrustManager.clearAllBiometricRecognized(FINGERPRINT, unlockedUser); + mTrustManager.clearAllBiometricRecognized(FACE, unlockedUser); mLogger.d("clearBiometricRecognized"); for (int i = 0; i < mCallbacks.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java index 76a7cad15419..8ae63c4e2f54 100644 --- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java @@ -17,24 +17,25 @@ package com.android.systemui; import android.app.AlertDialog; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.UserInfo; import android.os.UserHandle; -import android.util.Log; + +import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.settings.SecureSettings; +import java.util.concurrent.Executor; + import javax.inject.Inject; import dagger.assisted.Assisted; @@ -44,31 +45,66 @@ import dagger.assisted.AssistedInject; /** * Manages notification when a guest session is resumed. */ -public class GuestResumeSessionReceiver extends BroadcastReceiver { - - private static final String TAG = GuestResumeSessionReceiver.class.getSimpleName(); +public class GuestResumeSessionReceiver { @VisibleForTesting public static final String SETTING_GUEST_HAS_LOGGED_IN = "systemui.guest_has_logged_in"; @VisibleForTesting public AlertDialog mNewSessionDialog; + private final Executor mMainExecutor; private final UserTracker mUserTracker; private final SecureSettings mSecureSettings; - private final BroadcastDispatcher mBroadcastDispatcher; private final ResetSessionDialog.Factory mResetSessionDialogFactory; private final GuestSessionNotification mGuestSessionNotification; + @VisibleForTesting + public final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + cancelDialog(); + + UserInfo currentUser = mUserTracker.getUserInfo(); + if (!currentUser.isGuest()) { + return; + } + + int guestLoginState = mSecureSettings.getIntForUser( + SETTING_GUEST_HAS_LOGGED_IN, 0, newUser); + + if (guestLoginState == 0) { + // set 1 to indicate, 1st login + guestLoginState = 1; + mSecureSettings.putIntForUser(SETTING_GUEST_HAS_LOGGED_IN, guestLoginState, + newUser); + } else if (guestLoginState == 1) { + // set 2 to indicate, 2nd or later login + guestLoginState = 2; + mSecureSettings.putIntForUser(SETTING_GUEST_HAS_LOGGED_IN, guestLoginState, + newUser); + } + + mGuestSessionNotification.createPersistentNotification(currentUser, + (guestLoginState <= 1)); + + if (guestLoginState > 1) { + mNewSessionDialog = mResetSessionDialogFactory.create(newUser); + mNewSessionDialog.show(); + } + } + }; + @Inject public GuestResumeSessionReceiver( + @Main Executor mainExecutor, UserTracker userTracker, SecureSettings secureSettings, - BroadcastDispatcher broadcastDispatcher, GuestSessionNotification guestSessionNotification, ResetSessionDialog.Factory resetSessionDialogFactory) { + mMainExecutor = mainExecutor; mUserTracker = userTracker; mSecureSettings = secureSettings; - mBroadcastDispatcher = broadcastDispatcher; mGuestSessionNotification = guestSessionNotification; mResetSessionDialogFactory = resetSessionDialogFactory; } @@ -77,49 +113,7 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver { * Register this receiver with the {@link BroadcastDispatcher} */ public void register() { - IntentFilter f = new IntentFilter(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiver(this, f, null /* handler */, UserHandle.SYSTEM); - } - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (Intent.ACTION_USER_SWITCHED.equals(action)) { - cancelDialog(); - - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId == UserHandle.USER_NULL) { - Log.e(TAG, intent + " sent to " + TAG + " without EXTRA_USER_HANDLE"); - return; - } - - UserInfo currentUser = mUserTracker.getUserInfo(); - if (!currentUser.isGuest()) { - return; - } - - int guestLoginState = mSecureSettings.getIntForUser( - SETTING_GUEST_HAS_LOGGED_IN, 0, userId); - - if (guestLoginState == 0) { - // set 1 to indicate, 1st login - guestLoginState = 1; - mSecureSettings.putIntForUser(SETTING_GUEST_HAS_LOGGED_IN, guestLoginState, userId); - } else if (guestLoginState == 1) { - // set 2 to indicate, 2nd or later login - guestLoginState = 2; - mSecureSettings.putIntForUser(SETTING_GUEST_HAS_LOGGED_IN, guestLoginState, userId); - } - - mGuestSessionNotification.createPersistentNotification(currentUser, - (guestLoginState <= 1)); - - if (guestLoginState > 1) { - mNewSessionDialog = mResetSessionDialogFactory.create(userId); - mNewSessionDialog.show(); - } - } + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } private void cancelDialog() { diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 7e3b1389792c..02a6d7be7143 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -26,10 +26,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -45,7 +42,6 @@ import android.hardware.graphics.common.DisplayDecorationSupport; import android.os.Handler; import android.os.SystemProperties; import android.os.Trace; -import android.os.UserHandle; import android.provider.Settings.Secure; import android.util.DisplayUtils; import android.util.Log; @@ -68,7 +64,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.settingslib.Utils; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.decor.CutoutDecorProviderFactory; @@ -128,7 +123,6 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { private DisplayManager mDisplayManager; @VisibleForTesting protected boolean mIsRegistered; - private final BroadcastDispatcher mBroadcastDispatcher; private final Context mContext; private final Executor mMainExecutor; private final TunerService mTunerService; @@ -302,7 +296,6 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { public ScreenDecorations(Context context, @Main Executor mainExecutor, SecureSettings secureSettings, - BroadcastDispatcher broadcastDispatcher, TunerService tunerService, UserTracker userTracker, PrivacyDotViewController dotViewController, @@ -312,7 +305,6 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mContext = context; mMainExecutor = mainExecutor; mSecureSettings = secureSettings; - mBroadcastDispatcher = broadcastDispatcher; mTunerService = tunerService; mUserTracker = userTracker; mDotViewController = dotViewController; @@ -598,10 +590,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mColorInversionSetting.onChange(false); updateColorInversion(mColorInversionSetting.getValue()); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiver(mUserSwitchIntentReceiver, filter, - mExecutor, UserHandle.ALL); + mUserTracker.addCallback(mUserChangedCallback, mExecutor); mIsRegistered = true; } else { mMainExecutor.execute(() -> mTunerService.removeTunable(this)); @@ -610,7 +599,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mColorInversionSetting.setListening(false); } - mBroadcastDispatcher.unregisterReceiver(mUserSwitchIntentReceiver); + mUserTracker.removeCallback(mUserChangedCallback); mIsRegistered = false; } } @@ -897,18 +886,18 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { } } - private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - int newUserId = mUserTracker.getUserId(); - if (DEBUG) { - Log.d(TAG, "UserSwitched newUserId=" + newUserId); - } - // update color inversion setting to the new user - mColorInversionSetting.setUserId(newUserId); - updateColorInversion(mColorInversionSetting.getValue()); - } - }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + if (DEBUG) { + Log.d(TAG, "UserSwitched newUserId=" + newUser); + } + // update color inversion setting to the new user + mColorInversionSetting.setUserId(newUser); + updateColorInversion(mColorInversionSetting.getValue()); + } + }; private void updateColorInversion(int colorsInvertedValue) { mTintColor = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 998288a0a2c0..dab73e9bf289 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -52,6 +52,7 @@ import com.android.internal.util.ScreenshotHelper; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.recents.Recents; +import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -183,15 +184,18 @@ public class SystemActions implements CoreStartable { private final AccessibilityManager mA11yManager; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; private final NotificationShadeWindowController mNotificationShadeController; + private final ShadeController mShadeController; private final StatusBarWindowCallback mNotificationShadeCallback; private boolean mDismissNotificationShadeActionRegistered; @Inject public SystemActions(Context context, NotificationShadeWindowController notificationShadeController, + ShadeController shadeController, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, Optional<Recents> recentsOptional) { mContext = context; + mShadeController = shadeController; mRecentsOptional = recentsOptional; mReceiver = new SystemActionsBroadcastReceiver(); mLocale = mContext.getResources().getConfiguration().getLocales().get(0); @@ -529,9 +533,7 @@ public class SystemActions implements CoreStartable { } private void handleAccessibilityDismissNotificationShade() { - mCentralSurfacesOptionalLazy.get().ifPresent( - centralSurfaces -> centralSurfaces.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_NONE, false /* force */)); + mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); } private void handleDpadUp() { diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index 5616a00592f2..621b99d6804a 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -29,13 +29,15 @@ import android.os.UserHandle import android.util.Log import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper +import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper import com.android.systemui.people.widget.PeopleBackupHelper /** * Helper for backing up elements in SystemUI * - * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. - * The helper can be used to back up any element that is stored in [Context.getFilesDir]. + * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. The + * helper can be used to back up any element that is stored in [Context.getFilesDir] or + * [Context.getSharedPreferences]. * * After restoring is done, a [ACTION_RESTORE_FINISHED] intent will be send to SystemUI user 0, * indicating that restoring is finished for a given user. @@ -47,9 +49,11 @@ open class BackupHelper : BackupAgentHelper() { internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite" private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" + private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY = + "systemui.keyguard.quickaffordance.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" - private const val PERMISSION_SELF = "com.android.systemui.permission.SELF" + const val PERMISSION_SELF = "com.android.systemui.permission.SELF" } override fun onCreate(userHandle: UserHandle, operationType: Int) { @@ -67,17 +71,27 @@ open class BackupHelper : BackupAgentHelper() { } val keys = PeopleBackupHelper.getFilesToBackup() - addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper( - this, userHandle, keys.toTypedArray())) + addHelper( + PEOPLE_TILES_BACKUP_KEY, + PeopleBackupHelper(this, userHandle, keys.toTypedArray()) + ) + addHelper( + KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY, + KeyguardQuickAffordanceBackupHelper( + context = this, + userId = userHandle.identifier, + ), + ) } override fun onRestoreFinished() { super.onRestoreFinished() - val intent = Intent(ACTION_RESTORE_FINISHED).apply { - `package` = packageName - putExtra(Intent.EXTRA_USER_ID, userId) - flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY - } + val intent = + Intent(ACTION_RESTORE_FINISHED).apply { + `package` = packageName + putExtra(Intent.EXTRA_USER_ID, userId) + flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY + } sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF) } @@ -90,7 +104,9 @@ open class BackupHelper : BackupAgentHelper() { * @property lock a lock to hold while backing up and restoring the files. * @property context the context of the [BackupAgent] * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing + * ``` * actions to take + * ``` */ private class NoOverwriteFileBackupHelper( val lock: Any, @@ -115,23 +131,23 @@ open class BackupHelper : BackupAgentHelper() { data: BackupDataOutput?, newState: ParcelFileDescriptor? ) { - synchronized(lock) { - super.performBackup(oldState, data, newState) - } + synchronized(lock) { super.performBackup(oldState, data, newState) } } } } + private fun getPPControlsFile(context: Context): () -> Unit { return { val filesDir = context.filesDir val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS) if (file.exists()) { - val dest = Environment.buildPath(filesDir, - AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME) + val dest = + Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME) file.copyTo(dest) val jobScheduler = context.getSystemService(JobScheduler::class.java) jobScheduler?.schedule( - AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)) + AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context) + ) } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 4363b88bf21e..0c1cb9233c89 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -116,9 +116,9 @@ class AuthRippleController @Inject constructor( notificationShadeWindowController.setForcePluginOpen(false, this) } - fun showUnlockRipple(biometricSourceType: BiometricSourceType?) { + fun showUnlockRipple(biometricSourceType: BiometricSourceType) { if (!keyguardStateController.isShowing || - keyguardUpdateMonitor.userNeedsStrongAuth()) { + !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(biometricSourceType)) { return } @@ -246,7 +246,7 @@ class AuthRippleController @Inject constructor( object : KeyguardUpdateMonitorCallback() { override fun onBiometricAuthenticated( userId: Int, - biometricSourceType: BiometricSourceType?, + biometricSourceType: BiometricSourceType, isStrongBiometric: Boolean ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT) { @@ -255,14 +255,14 @@ class AuthRippleController @Inject constructor( showUnlockRipple(biometricSourceType) } - override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { + override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType) { if (biometricSourceType == BiometricSourceType.FINGERPRINT) { mView.retractDwellRipple() } } override fun onBiometricAcquired( - biometricSourceType: BiometricSourceType?, + biometricSourceType: BiometricSourceType, acquireInfo: Int ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT && diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 45595c80d36d..5a81bd3e01b6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -61,6 +61,11 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.biometrics.dagger.BiometricsBackground; +import com.android.systemui.biometrics.udfps.InteractionEvent; +import com.android.systemui.biometrics.udfps.NormalizedTouchData; +import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor; +import com.android.systemui.biometrics.udfps.TouchProcessor; +import com.android.systemui.biometrics.udfps.TouchProcessorResult; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; @@ -142,6 +147,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Nullable private final TouchProcessor mTouchProcessor; // 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. @@ -165,7 +171,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { // The current request from FingerprintService. Null if no current request. @Nullable UdfpsControllerOverlay mOverlay; - @Nullable private UdfpsEllipseDetection mUdfpsEllipseDetection; // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when // to turn off high brightness mode. To get around this limitation, the state of the AOD @@ -322,10 +327,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOverlayParams.equals(overlayParams)) { mOverlayParams = overlayParams; - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - mUdfpsEllipseDetection.updateOverlayParams(overlayParams); - } - final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer(); // When the bounds change it's always necessary to re-create the overlay's window with @@ -434,8 +435,99 @@ public class UdfpsController implements DozeReceiver, Dumpable { return portraitTouch; } + private void tryDismissingKeyguard() { + if (!mOnFingerDown) { + playStartHaptic(); + } + mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); + mAttemptedToDismissKeyguard = true; + } + @VisibleForTesting boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + return newOnTouch(requestId, event, fromUdfpsView); + } else { + return oldOnTouch(requestId, event, fromUdfpsView); + } + } + + private boolean newOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { + if (!fromUdfpsView) { + Log.e(TAG, "ignoring the touch injected from outside of UdfpsView"); + return false; + } + if (mOverlay == null) { + Log.w(TAG, "ignoring onTouch with null overlay"); + return false; + } + if (!mOverlay.matchesRequestId(requestId)) { + Log.w(TAG, "ignoring stale touch event: " + requestId + " current: " + + mOverlay.getRequestId()); + return false; + } + + final TouchProcessorResult result = mTouchProcessor.processTouch(event, mActivePointerId, + mOverlayParams); + if (result instanceof TouchProcessorResult.Failure) { + Log.w(TAG, ((TouchProcessorResult.Failure) result).getReason()); + return false; + } + + final TouchProcessorResult.ProcessedTouch processedTouch = + (TouchProcessorResult.ProcessedTouch) result; + final NormalizedTouchData data = processedTouch.getTouchData(); + + mActivePointerId = processedTouch.getPointerOnSensorId(); + switch (processedTouch.getEvent()) { + case DOWN: + if (shouldTryToDismissKeyguard()) { + tryDismissingKeyguard(); + } + onFingerDown(requestId, + data.getPointerId(), + data.getX(), + data.getY(), + data.getMinor(), + data.getMajor(), + data.getOrientation(), + data.getTime(), + data.getGestureStart(), + mStatusBarStateController.isDozing()); + break; + + case UP: + case CANCEL: + if (InteractionEvent.CANCEL.equals(processedTouch.getEvent())) { + Log.w(TAG, "This is a CANCEL event that's reported as an UP event!"); + } + mAttemptedToDismissKeyguard = false; + onFingerUp(requestId, + mOverlay.getOverlayView(), + data.getPointerId(), + data.getX(), + data.getY(), + data.getMinor(), + data.getMajor(), + data.getOrientation(), + data.getTime(), + data.getGestureStart(), + mStatusBarStateController.isDozing()); + mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION); + break; + + + default: + break; + } + + // We should only consume touches that are within the sensor. By returning "false" for + // touches outside of the sensor, we let other UI components consume these events and act on + // them appropriately. + return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds()); + } + + private boolean oldOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { if (mOverlay == null) { Log.w(TAG, "ignoring onTouch with null overlay"); return false; @@ -465,23 +557,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { mVelocityTracker.clear(); } - boolean withinSensorArea; - if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - // Ellipse detection - withinSensorArea = mUdfpsEllipseDetection.isGoodEllipseOverlap(event); - } else { - // Centroid with expanded overlay - withinSensorArea = - isWithinSensorArea(udfpsView, event.getRawX(), - event.getRawY(), fromUdfpsView); - } - } else { - // Centroid with sensor sized view - withinSensorArea = + final boolean withinSensorArea = isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView); - } - if (withinSensorArea) { Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0); Log.v(TAG, "onTouch | action down"); @@ -495,11 +572,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { } if ((withinSensorArea || fromUdfpsView) && shouldTryToDismissKeyguard()) { Log.v(TAG, "onTouch | dismiss keyguard ACTION_DOWN"); - if (!mOnFingerDown) { - playStartHaptic(); - } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); - mAttemptedToDismissKeyguard = true; + tryDismissingKeyguard(); } Trace.endSection(); @@ -512,33 +585,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { ? event.getPointerId(0) : event.findPointerIndex(mActivePointerId); if (idx == event.getActionIndex()) { - boolean actionMoveWithinSensorArea; - if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { - if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - // Ellipse detection - actionMoveWithinSensorArea = - mUdfpsEllipseDetection.isGoodEllipseOverlap(event); - } else { - // Centroid with expanded overlay - actionMoveWithinSensorArea = - isWithinSensorArea(udfpsView, event.getRawX(idx), - event.getRawY(idx), fromUdfpsView); - } - } else { - // Centroid with sensor sized view - actionMoveWithinSensorArea = - isWithinSensorArea(udfpsView, event.getX(idx), - event.getY(idx), fromUdfpsView); - } - + final boolean actionMoveWithinSensorArea = + isWithinSensorArea(udfpsView, event.getX(idx), event.getY(idx), + fromUdfpsView); if ((fromUdfpsView || actionMoveWithinSensorArea) && shouldTryToDismissKeyguard()) { Log.v(TAG, "onTouch | dismiss keyguard ACTION_MOVE"); - if (!mOnFingerDown) { - playStartHaptic(); - } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); - mAttemptedToDismissKeyguard = true; + tryDismissingKeyguard(); break; } // Map the touch to portrait mode if the device is in landscape mode. @@ -663,7 +716,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull ActivityLaunchAnimator activityLaunchAnimator, @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider, @NonNull @BiometricsBackground Executor biometricsExecutor, - @NonNull PrimaryBouncerInteractor primaryBouncerInteractor) { + @NonNull PrimaryBouncerInteractor primaryBouncerInteractor, + @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -704,6 +758,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { mBiometricExecutor = biometricsExecutor; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) + ? singlePointerTouchProcessor : null; + mDumpManager.registerDumpable(TAG, this); mOrientationListener = new BiometricDisplayListener( @@ -728,10 +785,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { udfpsHapticsSimulator.setUdfpsController(this); udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController); - - if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { - mUdfpsEllipseDetection = new UdfpsEllipseDetection(mOverlayParams); - } } /** @@ -913,7 +966,36 @@ public class UdfpsController implements DozeReceiver, Dumpable { return mOnFingerDown; } - private void onFingerDown(long requestId, int x, int y, float minor, float major) { + private void onFingerDown( + long requestId, + int x, + int y, + float minor, + float major) { + onFingerDown( + requestId, + MotionEvent.INVALID_POINTER_ID /* pointerId */, + x, + y, + minor, + major, + 0f /* orientation */, + 0L /* time */, + 0L /* gestureStart */, + false /* isAod */); + } + + private void onFingerDown( + long requestId, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { mExecution.assertIsMainThread(); if (mOverlay == null) { @@ -942,7 +1024,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mOnFingerDown = true; if (mAlternateTouchProvider != null) { mBiometricExecutor.execute(() -> { - mAlternateTouchProvider.onPointerDown(requestId, x, y, minor, major); + mAlternateTouchProvider.onPointerDown(requestId, (int) x, (int) y, minor, major); }); mFgExecutor.execute(() -> { if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { @@ -950,7 +1032,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { } }); } else { - mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, x, y, minor, major); + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, pointerId, x, y, + minor, major, orientation, time, gestureStart, isAod); + } else { + mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, (int) x, + (int) y, minor, major); + } } Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); final UdfpsView view = mOverlay.getOverlayView(); @@ -974,6 +1062,32 @@ public class UdfpsController implements DozeReceiver, Dumpable { } private void onFingerUp(long requestId, @NonNull UdfpsView view) { + onFingerUp( + requestId, + view, + MotionEvent.INVALID_POINTER_ID /* pointerId */, + 0f /* x */, + 0f /* y */, + 0f /* minor */, + 0f /* major */, + 0f /* orientation */, + 0L /* time */, + 0L /* gestureStart */, + false /* isAod */); + } + + private void onFingerUp( + long requestId, + @NonNull UdfpsView view, + int pointerId, + float x, + float y, + float minor, + float major, + float orientation, + long time, + long gestureStart, + boolean isAod) { mExecution.assertIsMainThread(); mActivePointerId = -1; mAcquiredReceived = false; @@ -988,7 +1102,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { } }); } else { - mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId); + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId, pointerId, x, + y, minor, major, orientation, time, gestureStart, isAod); + } else { + mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId); + } } for (Callback cb : mCallbacks) { cb.onFingerUp(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt deleted file mode 100644 index 8ae4775467df..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt +++ /dev/null @@ -1,92 +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.biometrics - -import android.graphics.Point -import android.graphics.Rect -import android.util.RotationUtils -import android.view.MotionEvent -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.sin - -private const val TAG = "UdfpsEllipseDetection" - -private const val NEEDED_POINTS = 2 - -class UdfpsEllipseDetection(overlayParams: UdfpsOverlayParams) { - var sensorRect = Rect() - var points: Array<Point> = emptyArray() - - init { - sensorRect = Rect(overlayParams.sensorBounds) - - points = calculateSensorPoints(sensorRect) - } - - fun updateOverlayParams(params: UdfpsOverlayParams) { - sensorRect = Rect(params.sensorBounds) - - val rot = params.rotation - RotationUtils.rotateBounds( - sensorRect, - params.naturalDisplayWidth, - params.naturalDisplayHeight, - rot - ) - - points = calculateSensorPoints(sensorRect) - } - - fun isGoodEllipseOverlap(event: MotionEvent): Boolean { - return points.count { checkPoint(event, it) } >= NEEDED_POINTS - } - - private fun checkPoint(event: MotionEvent, point: Point): Boolean { - // Calculate if sensor point is within ellipse - // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - - // yS))^2 / b^2) <= 1 - val a: Float = cos(event.orientation) * (point.x - event.rawX) - val b: Float = sin(event.orientation) * (point.y - event.rawY) - val c: Float = sin(event.orientation) * (point.x - event.rawX) - val d: Float = cos(event.orientation) * (point.y - event.rawY) - val result = - (a + b).pow(2) / (event.touchMinor / 2).pow(2) + - (c - d).pow(2) / (event.touchMajor / 2).pow(2) - - return result <= 1 - } -} - -fun calculateSensorPoints(sensorRect: Rect): Array<Point> { - val sensorX = sensorRect.centerX() - val sensorY = sensorRect.centerY() - val cornerOffset: Int = sensorRect.width() / 4 - val sideOffset: Int = sensorRect.width() / 3 - - return arrayOf( - Point(sensorX - cornerOffset, sensorY - cornerOffset), - Point(sensorX, sensorY - sideOffset), - Point(sensorX + cornerOffset, sensorY - cornerOffset), - Point(sensorX - sideOffset, sensorY), - Point(sensorX, sensorY), - Point(sensorX + sideOffset, sensorY), - Point(sensorX - cornerOffset, sensorY + cornerOffset), - Point(sensorX, sensorY + sideOffset), - Point(sensorX + cornerOffset, sensorY + cornerOffset) - ) -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt index 98d4c22d927d..7f3846ca4e40 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt @@ -7,17 +7,23 @@ import android.view.Surface.Rotation /** * Collection of parameters that define an under-display fingerprint sensor (UDFPS) overlay. * - * @property sensorBounds coordinates of the bounding box around the sensor, in natural orientation, - * in pixels, for the current resolution. - * @property naturalDisplayWidth width of the physical display, in natural orientation, in pixels, - * for the current resolution. - * @property naturalDisplayHeight height of the physical display, in natural orientation, in pixels, - * for the current resolution. - * @property scaleFactor ratio of a dimension in the current resolution to the corresponding - * dimension in the native resolution. - * @property rotation current rotation of the display. + * [sensorBounds] coordinates of the bounding box around the sensor in natural orientation, in + * pixels, for the current resolution. + * + * [overlayBounds] coordinates of the UI overlay in natural orientation, in pixels, for the current + * resolution. + * + * [naturalDisplayWidth] width of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [naturalDisplayHeight] height of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [scaleFactor] ratio of a dimension in the current resolution to the corresponding dimension in + * the native resolution. + * + * [rotation] current rotation of the display. */ - data class UdfpsOverlayParams( val sensorBounds: Rect = Rect(), val overlayBounds: Rect = Rect(), @@ -26,19 +32,23 @@ data class UdfpsOverlayParams( val scaleFactor: Float = 1f, @Rotation val rotation: Int = Surface.ROTATION_0 ) { + + /** Same as [sensorBounds], but in native resolution. */ + val nativeSensorBounds = Rect(sensorBounds).apply { scale(1f / scaleFactor) } + /** See [android.view.DisplayInfo.logicalWidth] */ - val logicalDisplayWidth - get() = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + val logicalDisplayWidth = + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { naturalDisplayHeight } else { naturalDisplayWidth } /** See [android.view.DisplayInfo.logicalHeight] */ - val logicalDisplayHeight - get() = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + val logicalDisplayHeight = + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { naturalDisplayWidth } else { naturalDisplayHeight } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt new file mode 100644 index 000000000000..001fed76c124 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt @@ -0,0 +1,43 @@ +/* + * 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.biometrics.dagger + +import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector +import com.android.systemui.biometrics.udfps.EllipseOverlapDetector +import com.android.systemui.biometrics.udfps.OverlapDetector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Module +import dagger.Provides + +/** Dagger module for all things UDFPS. TODO(b/260558624): Move to BiometricsModule. */ +@Module +interface UdfpsModule { + companion object { + + @Provides + @SysUISingleton + fun providesOverlapDetector(featureFlags: FeatureFlags): OverlapDetector { + return if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + EllipseOverlapDetector() + } else { + BoundingBoxOverlapDetector() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt new file mode 100644 index 000000000000..79a0acb8bbc1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Rect +import com.android.systemui.dagger.SysUISingleton + +/** Returns whether the touch coordinates are within the sensor's bounding box. */ +@SysUISingleton +class BoundingBoxOverlapDetector : OverlapDetector { + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean = + touchData.isWithinSensor(nativeSensorBounds) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt new file mode 100644 index 000000000000..857224290752 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -0,0 +1,71 @@ +/* + * 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.biometrics.udfps + +import android.graphics.Point +import android.graphics.Rect +import com.android.systemui.dagger.SysUISingleton +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin + +/** + * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap + * with the sensor. + */ +@SysUISingleton +class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetector { + + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + val points = calculateSensorPoints(nativeSensorBounds) + return points.count { checkPoint(it, touchData) } >= neededPoints + } + + private fun checkPoint(point: Point, touchData: NormalizedTouchData): Boolean { + // Calculate if sensor point is within ellipse + // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - + // yS))^2 / b^2) <= 1 + val a: Float = cos(touchData.orientation) * (point.x - touchData.x) + val b: Float = sin(touchData.orientation) * (point.y - touchData.y) + val c: Float = sin(touchData.orientation) * (point.x - touchData.x) + val d: Float = cos(touchData.orientation) * (point.y - touchData.y) + val result = + (a + b).pow(2) / (touchData.minor / 2).pow(2) + + (c - d).pow(2) / (touchData.major / 2).pow(2) + + return result <= 1 + } + + private fun calculateSensorPoints(sensorBounds: Rect): List<Point> { + val sensorX = sensorBounds.centerX() + val sensorY = sensorBounds.centerY() + val cornerOffset: Int = sensorBounds.width() / 4 + val sideOffset: Int = sensorBounds.width() / 3 + + return listOf( + Point(sensorX - cornerOffset, sensorY - cornerOffset), + Point(sensorX, sensorY - sideOffset), + Point(sensorX + cornerOffset, sensorY - cornerOffset), + Point(sensorX - sideOffset, sensorY), + Point(sensorX, sensorY), + Point(sensorX + sideOffset, sensorY), + Point(sensorX - cornerOffset, sensorY + cornerOffset), + Point(sensorX, sensorY + sideOffset), + Point(sensorX + cornerOffset, sensorY + cornerOffset) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt new file mode 100644 index 000000000000..6e47dadc4545 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/InteractionEvent.kt @@ -0,0 +1,50 @@ +/* + * 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.biometrics.udfps + +import android.view.MotionEvent + +/** Interaction event between a finger and the under-display fingerprint sensor (UDFPS). */ +enum class InteractionEvent { + /** + * A finger entered the sensor area. This can originate from either [MotionEvent.ACTION_DOWN] or + * [MotionEvent.ACTION_MOVE]. + */ + DOWN, + + /** + * A finger left the sensor area. This can originate from either [MotionEvent.ACTION_UP] or + * [MotionEvent.ACTION_MOVE]. + */ + UP, + + /** + * The touch reporting has stopped. This corresponds to [MotionEvent.ACTION_CANCEL]. This should + * not be confused with [UP]. If there was a finger on the sensor, it may or may not still be on + * the sensor. + */ + CANCEL, + + /** + * The interaction hasn't changed since the previous event. The can originate from any of + * [MotionEvent.ACTION_DOWN], [MotionEvent.ACTION_MOVE], or [MotionEvent.ACTION_UP] if one of + * these is true: + * - There was previously a finger on the sensor, and that finger is still on the sensor. + * - There was previously no finger on the sensor, and there still isn't. + */ + UNCHANGED, +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt new file mode 100644 index 000000000000..62bedc627b07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt @@ -0,0 +1,65 @@ +/* + * 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.biometrics.udfps + +import android.graphics.Rect +import android.view.MotionEvent + +/** Touch data in natural orientation and native resolution. */ +data class NormalizedTouchData( + + /** + * Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID + * is not available. + */ + val pointerId: Int, + + /** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */ + val x: Float, + + /** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */ + val y: Float, + + /** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */ + val minor: Float, + + /** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */ + val major: Float, + + /** [MotionEvent.getOrientation] mapped to natural orientation. */ + val orientation: Float, + + /** [MotionEvent.getEventTime]. */ + val time: Long, + + /** [MotionEvent.getDownTime]. */ + val gestureStart: Long, +) { + + /** + * [nativeSensorBounds] contains the location and dimensions of the sensor area in native + * resolution and natural orientation. + * + * Returns whether the coordinates of the given pointer are within the sensor's bounding box. + */ + fun isWithinSensor(nativeSensorBounds: Rect): Boolean { + return nativeSensorBounds.left <= x && + nativeSensorBounds.right >= x && + nativeSensorBounds.top <= y && + nativeSensorBounds.bottom >= y + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt new file mode 100644 index 000000000000..0fec8ffbaa0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Rect + +/** Determines whether the touch has a sufficient overlap with the sensor. */ +interface OverlapDetector { + fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt new file mode 100644 index 000000000000..338bf66d197e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -0,0 +1,164 @@ +/* + * 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.biometrics.udfps + +import android.graphics.PointF +import android.util.RotationUtils +import android.view.MotionEvent +import android.view.MotionEvent.INVALID_POINTER_ID +import android.view.Surface +import com.android.systemui.biometrics.UdfpsOverlayParams +import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure +import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** + * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. + */ +@SysUISingleton +class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: OverlapDetector) : + TouchProcessor { + + override fun processTouch( + event: MotionEvent, + previousPointerOnSensorId: Int, + overlayParams: UdfpsOverlayParams, + ): 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) + } + + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> processActionDown(preprocess()) + MotionEvent.ACTION_MOVE -> processActionMove(preprocess()) + MotionEvent.ACTION_UP -> processActionUp(preprocess()) + MotionEvent.ACTION_CANCEL -> + processActionCancel(event.normalize(pointerIndex = 0, overlayParams)) + else -> + Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked)) + } + } +} + +private data class PreprocessedTouch( + val data: NormalizedTouchData, + val previousPointerOnSensorId: Int, + val isGoodOverlap: Boolean, +) + +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) +} + +private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult { + return if (touch.isGoodOverlap) { + ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + } else { + val event = + if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) { + InteractionEvent.UP + } else { + InteractionEvent.UNCHANGED + } + ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data) + } +} + +private fun processActionCancel(data: NormalizedTouchData): TouchProcessorResult { + return ProcessedTouch(InteractionEvent.CANCEL, pointerOnSensorId = INVALID_POINTER_ID, data) +} + +/** + * Returns the touch information from the given [MotionEvent] with the relevant fields mapped to + * natural orientation and native resolution. + */ +private fun MotionEvent.normalize( + pointerIndex: Int, + overlayParams: UdfpsOverlayParams +): NormalizedTouchData { + val naturalTouch: PointF = rotateToNaturalOrientation(pointerIndex, overlayParams) + val nativeX = naturalTouch.x / overlayParams.scaleFactor + val nativeY = naturalTouch.y / overlayParams.scaleFactor + val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor + val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + 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), + time = eventTime, + gestureStart = downTime, + ) +} + +/** + * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device + * is in the [Surface.ROTATION_0] orientation. + */ +private fun MotionEvent.rotateToNaturalOrientation( + pointerIndex: Int, + overlayParams: UdfpsOverlayParams +): PointF { + val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) + val rot = overlayParams.rotation + if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + RotationUtils.rotatePointF( + touchPoint, + RotationUtils.deltaRotation(rot, Surface.ROTATION_0), + overlayParams.logicalDisplayWidth.toFloat(), + overlayParams.logicalDisplayHeight.toFloat() + ) + } + return touchPoint +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt new file mode 100644 index 000000000000..ffcebf9cff75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.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.biometrics.udfps + +import android.view.MotionEvent +import com.android.systemui.biometrics.UdfpsOverlayParams + +/** + * Determines whether a finger entered or left the area of the under-display fingerprint sensor + * (UDFPS). Maps the touch information from a [MotionEvent] to the orientation and scale independent + * [NormalizedTouchData]. + */ +interface TouchProcessor { + + /** + * [event] touch event to be processed. + * + * [previousPointerOnSensorId] pointerId for the finger that was on the sensor prior to this + * event. See [MotionEvent.getPointerId]. If there was no finger on the sensor, this should be + * set to [MotionEvent.INVALID_POINTER_ID]. + * + * [overlayParams] contains the location and dimensions of the sensor area, as well as the scale + * factor and orientation of the overlay. See [UdfpsOverlayParams]. + * + * Returns [TouchProcessorResult.ProcessedTouch] on success, and [TouchProcessorResult.Failure] + * on failure. + */ + fun processTouch( + event: MotionEvent, + previousPointerOnSensorId: Int, + overlayParams: UdfpsOverlayParams, + ): TouchProcessorResult +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt new file mode 100644 index 000000000000..be75bb0d3821 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessorResult.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.view.MotionEvent + +/** Contains all the possible returns types for [TouchProcessor.processTouch] */ +sealed class TouchProcessorResult { + + /** + * [event] whether a finger entered or left the sensor area. See [InteractionEvent]. + * + * [pointerOnSensorId] pointerId fof the finger that's currently on the sensor. See + * [MotionEvent.getPointerId]. If there is no finger on the sensor, the value is set to + * [MotionEvent.INVALID_POINTER_ID]. + * + * [touchData] relevant data from the MotionEvent, mapped to natural orientation and native + * resolution. See [NormalizedTouchData]. + */ + data class ProcessedTouch( + val event: InteractionEvent, + val pointerOnSensorId: Int, + val touchData: NormalizedTouchData + ) : TouchProcessorResult() + + /** [reason] the reason for the failure. */ + data class Failure(val reason: String = "") : TouchProcessorResult() +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 537cbc5a267d..a0a892de0085 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher" * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for * a given broadcast. * - * Use only for IntentFilters with actions and optionally categories. It does not support, - * permissions, schemes, data types, data authorities or priority different than 0. + * Use only for IntentFilters with actions and optionally categories. It does not support schemes, + * data types, data authorities or priority different than 0. + * * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery). * Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui * and doesn't need to worry about being killed. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt new file mode 100644 index 000000000000..3d10ab906f2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls + +import kotlinx.coroutines.flow.StateFlow + +/** Repository for Device controls related settings. */ +interface ControlsSettingsRepository { + /** Whether device controls activity can be shown above lockscreen for this user. */ + val canShowControlsInLockscreen: StateFlow<Boolean> + + /** Whether trivial controls can be actioned from the lockscreen for this user. */ + val allowActionOnTrivialControlsInLockscreen: StateFlow<Boolean> +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt new file mode 100644 index 000000000000..9dc422a09674 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt @@ -0,0 +1,90 @@ +/* + * 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.controls + +import android.provider.Settings +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.SettingObserver +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +/** + * This implementation uses an `@Application` [CoroutineScope] to provide hot flows for the values + * of the tracked settings. + */ +@SysUISingleton +class ControlsSettingsRepositoryImpl +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val userRepository: UserRepository, + private val secureSettings: SecureSettings +) : ControlsSettingsRepository { + + override val canShowControlsInLockscreen = + makeFlowForSetting(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS) + + override val allowActionOnTrivialControlsInLockscreen = + makeFlowForSetting(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) + + @OptIn(ExperimentalCoroutinesApi::class) + private fun makeFlowForSetting(setting: String): StateFlow<Boolean> { + return userRepository.selectedUserInfo + .distinctUntilChanged() + .flatMapLatest { userInfo -> + conflatedCallbackFlow { + val observer = + object : SettingObserver(secureSettings, null, setting, userInfo.id) { + override fun handleValueChanged( + value: Int, + observedChange: Boolean + ) { + trySend(value == 1) + } + } + observer.isListening = true + trySend(observer.value == 1) + awaitClose { observer.isListening = false } + } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + // When the observer starts listening, the flow will emit the current value + // so the initialValue here is irrelevant. + initialValue = false, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index bdfe1fbeb4b5..80c5f661f9a3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -33,7 +33,6 @@ import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.backup.BackupHelper -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController @@ -60,11 +59,10 @@ class ControlsControllerImpl @Inject constructor ( private val uiController: ControlsUiController, private val bindingController: ControlsBindingController, private val listingController: ControlsListingController, - private val broadcastDispatcher: BroadcastDispatcher, private val userFileManager: UserFileManager, + private val userTracker: UserTracker, optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, dumpManager: DumpManager, - userTracker: UserTracker ) : Dumpable, ControlsController { companion object { @@ -121,18 +119,15 @@ class ControlsControllerImpl @Inject constructor ( userChanging = false } - private val userSwitchReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_USER_SWITCHED) { - userChanging = true - val newUser = - UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId)) - if (currentUser == newUser) { - userChanging = false - return - } - setValuesForUser(newUser) + private val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + userChanging = true + val newUserHandle = UserHandle.of(newUser) + if (currentUser == newUserHandle) { + userChanging = false + return } + setValuesForUser(newUserHandle) } } @@ -234,12 +229,7 @@ class ControlsControllerImpl @Inject constructor ( dumpManager.registerDumpable(javaClass.name, this) resetFavorites() userChanging = false - broadcastDispatcher.registerReceiver( - userSwitchReceiver, - IntentFilter(Intent.ACTION_USER_SWITCHED), - executor, - UserHandle.ALL - ) + userTracker.addCallback(userTrackerCallback, executor) context.registerReceiver( restoreFinishedReceiver, IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), @@ -251,7 +241,7 @@ class ControlsControllerImpl @Inject constructor ( } fun destroy() { - broadcastDispatcher.unregisterReceiver(userSwitchReceiver) + userTracker.removeCallback(userTrackerCallback) context.unregisterReceiver(restoreFinishedReceiver) listingController.removeCallback(listingCallback) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt index 9e4a364562e5..77d0496e43db 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt @@ -16,13 +16,10 @@ package com.android.systemui.controls.dagger -import android.content.ContentResolver import android.content.Context -import android.database.ContentObserver -import android.os.UserHandle -import android.provider.Settings import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.systemui.controls.ControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl @@ -31,12 +28,10 @@ import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.settings.SecureSettings import dagger.Lazy +import kotlinx.coroutines.flow.StateFlow import java.util.Optional import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow /** * Pseudo-component to inject into classes outside `com.android.systemui.controls`. @@ -46,47 +41,26 @@ import kotlinx.coroutines.flow.asStateFlow */ @SysUISingleton class ControlsComponent @Inject constructor( - @ControlsFeatureEnabled private val featureEnabled: Boolean, - private val context: Context, - private val lazyControlsController: Lazy<ControlsController>, - private val lazyControlsUiController: Lazy<ControlsUiController>, - private val lazyControlsListingController: Lazy<ControlsListingController>, - private val lockPatternUtils: LockPatternUtils, - private val keyguardStateController: KeyguardStateController, - private val userTracker: UserTracker, - private val secureSettings: SecureSettings, - private val optionalControlsTileResourceConfiguration: - Optional<ControlsTileResourceConfiguration> + @ControlsFeatureEnabled private val featureEnabled: Boolean, + private val context: Context, + private val lazyControlsController: Lazy<ControlsController>, + private val lazyControlsUiController: Lazy<ControlsUiController>, + private val lazyControlsListingController: Lazy<ControlsListingController>, + private val lockPatternUtils: LockPatternUtils, + private val keyguardStateController: KeyguardStateController, + private val userTracker: UserTracker, + controlsSettingsRepository: ControlsSettingsRepository, + optionalControlsTileResourceConfiguration: Optional<ControlsTileResourceConfiguration> ) { - private val contentResolver: ContentResolver - get() = context.contentResolver - private val _canShowWhileLockedSetting = MutableStateFlow(false) - val canShowWhileLockedSetting = _canShowWhileLockedSetting.asStateFlow() + val canShowWhileLockedSetting: StateFlow<Boolean> = + controlsSettingsRepository.canShowControlsInLockscreen private val controlsTileResourceConfiguration: ControlsTileResourceConfiguration = optionalControlsTileResourceConfiguration.orElse( ControlsTileResourceConfigurationImpl() ) - val showWhileLockedObserver = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - updateShowWhileLocked() - } - } - - init { - if (featureEnabled) { - secureSettings.registerContentObserverForUser( - Settings.Secure.getUriFor(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS), - false, /* notifyForDescendants */ - showWhileLockedObserver, - UserHandle.USER_ALL - ) - updateShowWhileLocked() - } - } - fun getControlsController(): Optional<ControlsController> { return if (featureEnabled) Optional.of(lazyControlsController.get()) else Optional.empty() } @@ -127,11 +101,6 @@ class ControlsComponent @Inject constructor( return Visibility.AVAILABLE } - private fun updateShowWhileLocked() { - _canShowWhileLockedSetting.value = secureSettings.getIntForUser( - Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0, UserHandle.USER_CURRENT) != 0 - } - enum class Visibility { AVAILABLE, AVAILABLE_AFTER_UNLOCK, UNAVAILABLE } diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 6f58abdeed56..9ae605e30a83 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -20,6 +20,8 @@ import android.app.Activity import android.content.pm.PackageManager import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsMetricsLoggerImpl +import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.ControlsSettingsRepositoryImpl import com.android.systemui.controls.controller.ControlsBindingController import com.android.systemui.controls.controller.ControlsBindingControllerImpl import com.android.systemui.controls.controller.ControlsController @@ -83,6 +85,11 @@ abstract class ControlsModule { abstract fun provideUiController(controller: ControlsUiControllerImpl): ControlsUiController @Binds + abstract fun provideSettingsManager( + manager: ControlsSettingsRepositoryImpl + ): ControlsSettingsRepository + + @Binds abstract fun provideMetricsLogger(logger: ControlsMetricsLoggerImpl): ControlsMetricsLogger @Binds diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index b8a00133c728..8472ca0731d7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -24,9 +24,6 @@ import android.app.PendingIntent import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler import android.os.UserHandle import android.os.VibrationEffect import android.provider.Settings.Secure @@ -40,6 +37,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger +import com.android.systemui.controls.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -68,17 +66,17 @@ class ControlActionCoordinatorImpl @Inject constructor( private val vibrator: VibratorHelper, private val secureSettings: SecureSettings, private val userContextProvider: UserContextProvider, - @Main mainHandler: Handler + private val controlsSettingsRepository: ControlsSettingsRepository, ) : ControlActionCoordinator { private var dialog: Dialog? = null private var pendingAction: Action? = null private var actionsInProgress = mutableSetOf<String>() private val isLocked: Boolean get() = !keyguardStateController.isUnlocked() - private var mAllowTrivialControls: Boolean = secureSettings.getIntForUser( - Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 0, UserHandle.USER_CURRENT) != 0 - private var mShowDeviceControlsInLockscreen: Boolean = secureSettings.getIntForUser( - Secure.LOCKSCREEN_SHOW_CONTROLS, 0, UserHandle.USER_CURRENT) != 0 + private val allowTrivialControls: Boolean + get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value + private val showDeviceControlsInLockscreen: Boolean + get() = controlsSettingsRepository.canShowControlsInLockscreen.value override lateinit var activityContext: Context companion object { @@ -86,38 +84,6 @@ class ControlActionCoordinatorImpl @Inject constructor( private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 } - init { - val lockScreenShowControlsUri = - secureSettings.getUriFor(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) - val showControlsUri = - secureSettings.getUriFor(Secure.LOCKSCREEN_SHOW_CONTROLS) - val controlsContentObserver = object : ContentObserver(mainHandler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - when (uri) { - lockScreenShowControlsUri -> { - mAllowTrivialControls = secureSettings.getIntForUser( - Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, - 0, UserHandle.USER_CURRENT) != 0 - } - showControlsUri -> { - mShowDeviceControlsInLockscreen = secureSettings - .getIntForUser(Secure.LOCKSCREEN_SHOW_CONTROLS, - 0, UserHandle.USER_CURRENT) != 0 - } - } - } - } - secureSettings.registerContentObserverForUser( - lockScreenShowControlsUri, - false /* notifyForDescendants */, controlsContentObserver, UserHandle.USER_ALL - ) - secureSettings.registerContentObserverForUser( - showControlsUri, - false /* notifyForDescendants */, controlsContentObserver, UserHandle.USER_ALL - ) - } - override fun closeDialogs() { dialog?.dismiss() dialog = null @@ -224,7 +190,7 @@ class ControlActionCoordinatorImpl @Inject constructor( @AnyThread @VisibleForTesting fun bouncerOrRun(action: Action) { - val authRequired = action.authIsRequired || !mAllowTrivialControls + val authRequired = action.authIsRequired || !allowTrivialControls if (keyguardStateController.isShowing() && authRequired) { if (isLocked) { @@ -282,7 +248,7 @@ class ControlActionCoordinatorImpl @Inject constructor( PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) if (attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || - (mShowDeviceControlsInLockscreen && mAllowTrivialControls)) { + (showDeviceControlsInLockscreen && allowTrivialControls)) { return } val builder = AlertDialog @@ -304,7 +270,7 @@ class ControlActionCoordinatorImpl @Inject constructor( true } - if (mShowDeviceControlsInLockscreen) { + if (showDeviceControlsInLockscreen) { dialog = builder .setTitle(R.string.controls_settings_trivial_controls_dialog_title) .setMessage(R.string.controls_settings_trivial_controls_dialog_message) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index d60a22204b3d..3d8e4cb71aca 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -19,7 +19,6 @@ package com.android.systemui.dagger; import android.content.BroadcastReceiver; import com.android.systemui.GuestResetOrExitSessionReceiver; -import com.android.systemui.GuestResumeSessionReceiver; import com.android.systemui.media.dialog.MediaOutputDialogReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetPinnedReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; @@ -106,15 +105,6 @@ public abstract class DefaultBroadcastReceiverBinder { */ @Binds @IntoMap - @ClassKey(GuestResumeSessionReceiver.class) - public abstract BroadcastReceiver bindGuestResumeSessionReceiver( - GuestResumeSessionReceiver broadcastReceiver); - - /** - * - */ - @Binds - @IntoMap @ClassKey(GuestResetOrExitSessionReceiver.class) public abstract BroadcastReceiver bindGuestResetOrExitSessionReceiver( GuestResetOrExitSessionReceiver broadcastReceiver); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 95919c6b2c0d..b8e66735c740 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -33,6 +33,7 @@ import com.android.systemui.assist.AssistModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; +import com.android.systemui.biometrics.dagger.UdfpsModule; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.controls.dagger.ControlsModule; @@ -156,6 +157,7 @@ import dagger.Provides; TelephonyRepositoryModule.class, TemporaryDisplayModule.class, TunerModule.class, + UdfpsModule.class, UserModule.class, UtilModule.class, NoteTaskModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 0b69b80689e0..5daf1ceaf592 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -29,12 +29,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.hardware.display.AmbientDisplayConfiguration; import android.os.SystemClock; -import android.os.UserHandle; import android.text.format.Formatter; import android.util.IndentingPrintWriter; import android.util.Log; import android.view.Display; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEvent; @@ -100,6 +101,7 @@ public class DozeTriggers implements DozeMachine.Part { private final BroadcastDispatcher mBroadcastDispatcher; private final AuthController mAuthController; private final KeyguardStateController mKeyguardStateController; + private final UserTracker mUserTracker; private final UiEventLogger mUiEventLogger; private long mNotificationPulseTime; @@ -110,6 +112,14 @@ public class DozeTriggers implements DozeMachine.Part { private boolean mWantTouchScreenSensors; private boolean mWantSensors; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mDozeSensors.onUserSwitched(); + } + }; + @VisibleForTesting public enum DozingUpdateUiEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Dozing updated due to notification.") @@ -210,6 +220,7 @@ public class DozeTriggers implements DozeMachine.Part { mAuthController = authController; mUiEventLogger = uiEventLogger; mKeyguardStateController = keyguardStateController; + mUserTracker = userTracker; } @Override @@ -234,7 +245,7 @@ public class DozeTriggers implements DozeMachine.Part { return; } mNotificationPulseTime = SystemClock.elapsedRealtime(); - if (!mConfig.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) { + if (!mConfig.pulseOnNotificationEnabled(mUserTracker.getUserId())) { runIfNotNull(onPulseSuppressedListener); mDozeLog.tracePulseDropped("pulseOnNotificationsDisabled"); return; @@ -490,12 +501,14 @@ public class DozeTriggers implements DozeMachine.Part { mBroadcastReceiver.register(mBroadcastDispatcher); mDockManager.addListener(mDockEventListener); mDozeHost.addCallback(mHostCallback); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); } private void unregisterCallbacks() { mBroadcastReceiver.unregister(mBroadcastDispatcher); mDozeHost.removeCallback(mHostCallback); mDockManager.removeListener(mDockEventListener); + mUserTracker.removeCallback(mUserChangedCallback); } private void stopListeningToAllTriggers() { @@ -620,9 +633,6 @@ public class DozeTriggers implements DozeMachine.Part { requestPulse(DozeLog.PULSE_REASON_INTENT, false, /* performedProxCheck */ null /* onPulseSuppressedListener */); } - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - mDozeSensors.onUserSwitched(); - } } public void register(BroadcastDispatcher broadcastDispatcher) { @@ -630,7 +640,6 @@ public class DozeTriggers implements DozeMachine.Part { return; } IntentFilter filter = new IntentFilter(PULSE_ACTION); - filter.addAction(Intent.ACTION_USER_SWITCHED); broadcastDispatcher.registerReceiver(this, filter); mRegistered = true; } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java index 48159aed524e..46ce7a90f5a3 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java @@ -192,9 +192,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll break; } - // Add margin if specified by the complication. Otherwise add default margin - // between complications. - if (mLayoutParams.isMarginSpecified() || !isRoot) { + if (!isRoot) { final int margin = mLayoutParams.getMargin(mDefaultMargin); switch(direction) { case ComplicationLayoutParams.DIRECTION_DOWN: @@ -213,6 +211,19 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll } }); + if (mLayoutParams.constraintSpecified()) { + switch (direction) { + case ComplicationLayoutParams.DIRECTION_START: + case ComplicationLayoutParams.DIRECTION_END: + params.matchConstraintMaxWidth = mLayoutParams.getConstraint(); + break; + case ComplicationLayoutParams.DIRECTION_UP: + case ComplicationLayoutParams.DIRECTION_DOWN: + params.matchConstraintMaxHeight = mLayoutParams.getConstraint(); + break; + } + } + mView.setLayoutParams(params); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java index 4fae68d57ee8..1755cb92da70 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java @@ -52,6 +52,7 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { private static final int LAST_POSITION = POSITION_END; private static final int MARGIN_UNSPECIFIED = 0xFFFFFFFF; + private static final int CONSTRAINT_UNSPECIFIED = 0xFFFFFFFF; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "DIRECTION_" }, value = { @@ -81,6 +82,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { private final int mMargin; + private final int mConstraint; + private final boolean mSnapToGuide; // Do not allow specifying opposite positions @@ -110,7 +113,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { */ public ComplicationLayoutParams(int width, int height, @Position int position, @Direction int direction, int weight) { - this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, false); + this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, CONSTRAINT_UNSPECIFIED, + false); } /** @@ -127,7 +131,27 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { */ public ComplicationLayoutParams(int width, int height, @Position int position, @Direction int direction, int weight, int margin) { - this(width, height, position, direction, weight, margin, false); + this(width, height, position, direction, weight, margin, CONSTRAINT_UNSPECIFIED, false); + } + + /** + * Constructs a {@link ComplicationLayoutParams}. + * @param width The width {@link android.view.View.MeasureSpec} for the view. + * @param height The height {@link android.view.View.MeasureSpec} for the view. + * @param position The place within the parent container where the view should be positioned. + * @param direction The direction the view should be laid out from either the parent container + * or preceding view. + * @param weight The weight that should be considered for this view when compared to other + * views. This has an impact on the placement of the view but not the rendering of + * the view. + * @param margin The margin to apply between complications. + * @param constraint The max width or height the complication is allowed to spread, depending on + * its direction. For horizontal directions, this would be applied on width, + * and for vertical directions, height. + */ + public ComplicationLayoutParams(int width, int height, @Position int position, + @Direction int direction, int weight, int margin, int constraint) { + this(width, height, position, direction, weight, margin, constraint, false); } /** @@ -148,7 +172,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { */ public ComplicationLayoutParams(int width, int height, @Position int position, @Direction int direction, int weight, boolean snapToGuide) { - this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, snapToGuide); + this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, CONSTRAINT_UNSPECIFIED, + snapToGuide); } /** @@ -162,6 +187,9 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { * views. This has an impact on the placement of the view but not the rendering of * the view. * @param margin The margin to apply between complications. + * @param constraint The max width or height the complication is allowed to spread, depending on + * its direction. For horizontal directions, this would be applied on width, + * and for vertical directions, height. * @param snapToGuide When set to {@code true}, the dimension perpendicular to the direction * will be automatically set to align with a predetermined guide for that * side. For example, if the complication is aligned to the top end and @@ -169,7 +197,7 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { * from the end of the parent to the guide. */ public ComplicationLayoutParams(int width, int height, @Position int position, - @Direction int direction, int weight, int margin, boolean snapToGuide) { + @Direction int direction, int weight, int margin, int constraint, boolean snapToGuide) { super(width, height); if (!validatePosition(position)) { @@ -187,6 +215,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { mMargin = margin; + mConstraint = constraint; + mSnapToGuide = snapToGuide; } @@ -199,6 +229,7 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { mDirection = source.mDirection; mWeight = source.mWeight; mMargin = source.mMargin; + mConstraint = source.mConstraint; mSnapToGuide = source.mSnapToGuide; } @@ -261,13 +292,6 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { } /** - * Returns whether margin has been specified by the complication. - */ - public boolean isMarginSpecified() { - return mMargin != MARGIN_UNSPECIFIED; - } - - /** * Returns the margin to apply between complications, or the given default if no margin is * specified. */ @@ -276,6 +300,21 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { } /** + * Returns whether the horizontal or vertical constraint has been specified. + */ + public boolean constraintSpecified() { + return mConstraint != CONSTRAINT_UNSPECIFIED; + } + + /** + * Returns the horizontal or vertical constraint of the complication, depending its direction. + * For horizontal directions, this is the max width, and for vertical directions, max height. + */ + public int getConstraint() { + return mConstraint; + } + + /** * Returns whether the complication's dimension perpendicular to direction should be * automatically set. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java index c01cf43eae74..ee0051220787 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -32,6 +32,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; +import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.dagger.ControlsComponent; @@ -151,7 +152,7 @@ public class DreamHomeControlsComplication implements Complication { @Inject DreamHomeControlsChipViewHolder( DreamHomeControlsChipViewController dreamHomeControlsChipViewController, - @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view, + @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) View view, @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams ) { mView = view; @@ -174,7 +175,7 @@ public class DreamHomeControlsComplication implements Complication { /** * Controls behavior of the dream complication. */ - static class DreamHomeControlsChipViewController extends ViewController<ImageView> { + static class DreamHomeControlsChipViewController extends ViewController<View> { private static final String TAG = "DreamHomeControlsCtrl"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -203,7 +204,7 @@ public class DreamHomeControlsComplication implements Complication { @Inject DreamHomeControlsChipViewController( - @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view, + @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) View view, ActivityStarter activityStarter, Context context, ControlsComponent controlsComponent, @@ -218,9 +219,10 @@ public class DreamHomeControlsComplication implements Complication { @Override protected void onViewAttached() { - mView.setImageResource(mControlsComponent.getTileImageId()); - mView.setContentDescription(mContext.getString(mControlsComponent.getTileTitleId())); - mView.setOnClickListener(this::onClickHomeControls); + final ImageView chip = mView.findViewById(R.id.home_controls_chip); + chip.setImageResource(mControlsComponent.getTileImageId()); + chip.setContentDescription(mContext.getString(mControlsComponent.getTileTitleId())); + chip.setOnClickListener(this::onClickHomeControls); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java index 7d9f1059f3b8..5290e44aa7fb 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java @@ -45,11 +45,12 @@ public interface DreamClockTimeComplicationModule { @Provides @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) static View provideComplicationView(LayoutInflater layoutInflater) { - final TextClock view = Preconditions.checkNotNull((TextClock) + final View view = Preconditions.checkNotNull( layoutInflater.inflate(R.layout.dream_overlay_complication_clock_time, null, false), "R.layout.dream_overlay_complication_clock_time did not properly inflated"); - view.setFontVariationSettings(TAG_WEIGHT + WEIGHT); + ((TextClock) view.findViewById(R.id.time_view)).setFontVariationSettings( + TAG_WEIGHT + WEIGHT); return view; } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java index cf05d2d9cda0..a7aa97f74e31 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java @@ -19,7 +19,7 @@ package com.android.systemui.dreams.complication.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; import android.view.LayoutInflater; -import android.widget.ImageView; +import android.view.View; import com.android.systemui.R; import com.android.systemui.dreams.complication.DreamHomeControlsComplication; @@ -74,8 +74,8 @@ public interface DreamHomeControlsComplicationComponent { @Provides @DreamHomeControlsComplicationScope @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) - static ImageView provideHomeControlsChipView(LayoutInflater layoutInflater) { - return (ImageView) layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip, + static View provideHomeControlsChipView(LayoutInflater layoutInflater) { + return layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip, null, false); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java index a514c47f1fc1..9b954f5f0c4a 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java @@ -47,10 +47,10 @@ public interface RegisteredComplicationsModule { String DREAM_MEDIA_ENTRY_LAYOUT_PARAMS = "media_entry_layout_params"; int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1; - int DREAM_SMARTSPACE_COMPLICATION_WEIGHT = 0; + int DREAM_SMARTSPACE_COMPLICATION_WEIGHT = 2; int DREAM_MEDIA_COMPLICATION_WEIGHT = 0; - int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 2; - int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 1; + int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 4; + int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 3; /** * Provides layout parameters for the clock time complication. @@ -72,17 +72,14 @@ public interface RegisteredComplicationsModule { */ @Provides @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS) - static ComplicationLayoutParams provideHomeControlsChipLayoutParams(@Main Resources res) { + static ComplicationLayoutParams provideHomeControlsChipLayoutParams() { return new ComplicationLayoutParams( - res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), - res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, ComplicationLayoutParams.POSITION_BOTTOM | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_UP, - DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT, - // Add margin to the bottom of home controls to horizontally align with smartspace. - res.getDimensionPixelSize( - R.dimen.dream_overlay_complication_home_controls_padding)); + ComplicationLayoutParams.DIRECTION_END, + DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT); } /** @@ -106,12 +103,14 @@ public interface RegisteredComplicationsModule { @Provides @Named(DREAM_SMARTSPACE_LAYOUT_PARAMS) static ComplicationLayoutParams provideSmartspaceLayoutParams(@Main Resources res) { - return new ComplicationLayoutParams(0, + return new ComplicationLayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ComplicationLayoutParams.POSITION_BOTTOM | ComplicationLayoutParams.POSITION_START, ComplicationLayoutParams.DIRECTION_END, DREAM_SMARTSPACE_COMPLICATION_WEIGHT, - res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding)); + res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding), + res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_max_width)); } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6595b80eea55..886f690c6d81 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -207,15 +207,6 @@ object Flags { unreleasedFlag(508, "qs_secondary_data_sub_info", teamfood = true) // 600- status bar - // TODO(b/254512623): Tracking Bug - @Deprecated("Replaced by mobile and wifi specific flags.") - val NEW_STATUS_BAR_PIPELINE_BACKEND = - unreleasedFlag(604, "new_status_bar_pipeline_backend", teamfood = false) - - // TODO(b/254512660): Tracking Bug - @Deprecated("Replaced by mobile and wifi specific flags.") - val NEW_STATUS_BAR_PIPELINE_FRONTEND = - unreleasedFlag(605, "new_status_bar_pipeline_frontend", teamfood = false) // TODO(b/256614753): Tracking Bug val NEW_STATUS_BAR_MOBILE_ICONS = unreleasedFlag(606, "new_status_bar_mobile_icons") @@ -233,6 +224,10 @@ object Flags { // TODO(b/256623670): Tracking Bug @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon") + // TODO(b/260881289): Tracking Bug + val NEW_STATUS_BAR_ICONS_DEBUG_COLORING = + unreleasedFlag(611, "new_status_bar_icons_debug_coloring") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip") @@ -348,6 +343,12 @@ object Flags { // TODO(b/256873975): Tracking Bug @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar") + // TODO(b/260271148): Tracking bug + @Keep + @JvmField + val WM_DESKTOP_WINDOWING_2 = + sysPropBooleanFlag(1112, "persist.wm.debug.desktop_mode_2", default = false) + // 1200 - predictive back @Keep @JvmField @@ -371,7 +372,7 @@ object Flags { // TODO(b/255854141): Tracking Bug @JvmField val WM_ENABLE_PREDICTIVE_BACK_SYSUI = - unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = false) + unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true) // 1300 - screenshots // TODO(b/254512719): Tracking Bug @@ -397,17 +398,15 @@ object Flags { // 1700 - clipboard @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor") - @JvmField - val CLIPBOARD_REMOTE_BEHAVIOR = - unreleasedFlag(1701, "clipboard_remote_behavior", teamfood = true) + @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") // 1800 - shade container @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true) - // 1900 - note task - @JvmField val NOTE_TASKS = sysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks") + // 1900 + @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag") // 2000 - device controls @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true) @@ -424,6 +423,12 @@ object Flags { // 2300 - stylus @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used") + // 2400 - performance tools and debugging info + // TODO(b/238923086): Tracking Bug + @JvmField + val WARN_ON_BLOCKING_BINDER_TRANSACTIONS = + unreleasedFlag(2400, "warn_on_blocking_binder_transactions") + // TODO(b259590361): Tracking bug val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 29febb6dd0d9..4ae37c51f278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -29,7 +29,7 @@ import android.util.Log import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject import kotlinx.coroutines.runBlocking diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 8846bbd4754d..c332a0d66294 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -249,6 +249,7 @@ public class KeyguardService extends Service { synchronized (mFinishCallbacks) { if (mFinishCallbacks.remove(transition) == null) return; } + info.releaseAllSurfaces(); Slog.d(TAG, "Finish IRemoteAnimationRunner."); finishCallback.onTransitionFinished(null /* wct */, null /* t */); } @@ -264,6 +265,8 @@ public class KeyguardService extends Service { synchronized (mFinishCallbacks) { origFinishCB = mFinishCallbacks.remove(transition); } + info.releaseAllSurfaces(); + t.close(); if (origFinishCB == null) { // already finished (or not started yet), so do nothing. return; @@ -459,12 +462,15 @@ public class KeyguardService extends Service { t.apply(); mBinder.setOccluded(true /* isOccluded */, true /* animate */); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + info.releaseAllSurfaces(); } @Override public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { + t.close(); + info.releaseAllSurfaces(); } }; @@ -476,12 +482,15 @@ public class KeyguardService extends Service { t.apply(); mBinder.setOccluded(false /* isOccluded */, true /* animate */); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + info.releaseAllSurfaces(); } @Override public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { + t.close(); + info.releaseAllSurfaces(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 8403fe650687..6ed555056cb1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -869,7 +869,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onLaunchAnimationEnd(boolean launchIsFullScreen) { if (launchIsFullScreen) { - mCentralSurfaces.instantCollapseNotificationPanel(); + mShadeController.get().instantCollapseShade(); } mOccludeAnimationPlaying = false; 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 3c09aab60443..dbc376e62950 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,14 +26,17 @@ 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 dagger.Lazy +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @SysUISingleton -class CameraQuickAffordanceConfig @Inject constructor( - @Application private val context: Context, - private val cameraGestureHelper: CameraGestureHelper, +class CameraQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val cameraGestureHelper: Lazy<CameraGestureHelper>, ) : KeyguardQuickAffordanceConfig { override val key: String @@ -46,17 +49,23 @@ class CameraQuickAffordanceConfig @Inject constructor( get() = com.android.internal.R.drawable.perm_group_camera override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> - get() = flowOf( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = Icon.Resource( + get() = + flowOf( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( com.android.internal.R.drawable.perm_group_camera, ContentDescription.Resource(R.string.accessibility_camera_button) - ) + ) + ) ) - ) - override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult { - cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + cameraGestureHelper + .get() + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } -}
\ 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 49527d32d229..62fe80a82908 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 @@ -21,50 +21,52 @@ import android.content.Context import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 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.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.statusbar.policy.FlashlightController +import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import javax.inject.Inject @SysUISingleton -class FlashlightQuickAffordanceConfig @Inject constructor( - @Application private val context: Context, - private val flashlightController: FlashlightController, +class FlashlightQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val flashlightController: FlashlightController, ) : KeyguardQuickAffordanceConfig { private sealed class FlashlightState { abstract fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState - object On: FlashlightState() { + object On : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( - R.drawable.ic_flashlight_on, + R.drawable.qs_flashlight_icon_on, ContentDescription.Resource(R.string.quick_settings_flashlight_label) ), ActivationState.Active ) } - object OffAvailable: FlashlightState() { + object OffAvailable : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( - R.drawable.ic_flashlight_off, + R.drawable.qs_flashlight_icon_off, ContentDescription.Resource(R.string.quick_settings_flashlight_label) ), ActivationState.Inactive ) } - object Unavailable: FlashlightState() { + object Unavailable : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Hidden } @@ -77,57 +79,57 @@ class FlashlightQuickAffordanceConfig @Inject constructor( get() = context.getString(R.string.quick_settings_flashlight_label) override val pickerIconResourceId: Int - get() = if (flashlightController.isEnabled) { - R.drawable.ic_flashlight_on - } else { - R.drawable.ic_flashlight_off - } + get() = R.drawable.ic_flashlight_off override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = - conflatedCallbackFlow { - val flashlightCallback = object : FlashlightController.FlashlightListener { - override fun onFlashlightChanged(enabled: Boolean) { - trySendWithFailureLogging( - if (enabled) { - FlashlightState.On.toLockScreenState() - } else { - FlashlightState.OffAvailable.toLockScreenState() - }, - TAG - ) - } - - override fun onFlashlightError() { - trySendWithFailureLogging(FlashlightState.OffAvailable.toLockScreenState(), TAG) - } - - override fun onFlashlightAvailabilityChanged(available: Boolean) { - trySendWithFailureLogging( - if (!available) { - FlashlightState.Unavailable.toLockScreenState() - } else { - if (flashlightController.isEnabled) { - FlashlightState.On.toLockScreenState() - } else { - FlashlightState.OffAvailable.toLockScreenState() - } - }, - TAG - ) - } + conflatedCallbackFlow { + val flashlightCallback = + object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) { + trySendWithFailureLogging( + if (enabled) { + FlashlightState.On.toLockScreenState() + } else { + FlashlightState.OffAvailable.toLockScreenState() + }, + TAG + ) + } + + override fun onFlashlightError() { + trySendWithFailureLogging( + FlashlightState.OffAvailable.toLockScreenState(), + TAG + ) + } + + override fun onFlashlightAvailabilityChanged(available: Boolean) { + trySendWithFailureLogging( + if (!available) { + FlashlightState.Unavailable.toLockScreenState() + } else { + if (flashlightController.isEnabled) { + FlashlightState.On.toLockScreenState() + } else { + FlashlightState.OffAvailable.toLockScreenState() + } + }, + TAG + ) + } + } + + flashlightController.addCallback(flashlightCallback) + + awaitClose { flashlightController.removeCallback(flashlightCallback) } } - flashlightController.addCallback(flashlightCallback) - - awaitClose { - flashlightController.removeCallback(flashlightCallback) - } - } - - override fun onTriggered(expandable: Expandable?): - KeyguardQuickAffordanceConfig.OnTriggeredResult { - flashlightController - .setFlashlight(flashlightController.isAvailable && !flashlightController.isEnabled) + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + flashlightController.setFlashlight( + flashlightController.isAvailable && !flashlightController.isEnabled + ) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } @@ -141,4 +143,4 @@ class FlashlightQuickAffordanceConfig @Inject constructor( companion object { private const val TAG = "FlashlightQuickAffordanceConfig" } -}
\ No newline at end of file +} 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 3013227c21c0..072cfb140e62 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 @@ -17,27 +17,35 @@ package com.android.systemui.keyguard.data.quickaffordance +import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @Module -object KeyguardDataQuickAffordanceModule { - @Provides - @ElementsIntoSet - fun quickAffordanceConfigs( - flashlight: FlashlightQuickAffordanceConfig, - home: HomeControlsKeyguardQuickAffordanceConfig, - quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, - qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, - camera: CameraQuickAffordanceConfig, - ): Set<KeyguardQuickAffordanceConfig> { - return setOf( - camera, - flashlight, - home, - quickAccessWallet, - qrCodeScanner, - ) +interface KeyguardDataQuickAffordanceModule { + @Binds + fun providerClientFactory( + impl: KeyguardQuickAffordanceProviderClientFactoryImpl, + ): KeyguardQuickAffordanceProviderClientFactory + + companion object { + @Provides + @ElementsIntoSet + fun quickAffordanceConfigs( + flashlight: FlashlightQuickAffordanceConfig, + home: HomeControlsKeyguardQuickAffordanceConfig, + quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, + qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, + camera: CameraQuickAffordanceConfig, + ): Set<KeyguardQuickAffordanceConfig> { + return setOf( + camera, + flashlight, + home, + quickAccessWallet, + qrCodeScanner, + ) + } } } 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 4477310dca41..98b1a731b82c 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 @@ -21,7 +21,7 @@ import android.content.Intent import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.shared.quickaffordance.ActivationState -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt index 766096f1fa2b..72747f68bbbd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt @@ -67,7 +67,7 @@ constructor( @Application private val scope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val secureSettings: SecureSettings, - private val selectionsManager: KeyguardQuickAffordanceSelectionManager, + private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager, ) { companion object { private val BINDINGS = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt new file mode 100644 index 000000000000..006678546de8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.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.quickaffordance + +import android.content.Context +import android.content.IntentFilter +import android.content.SharedPreferences +import com.android.systemui.R +import com.android.systemui.backup.BackupHelper +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.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onStart + +/** + * Manages and provides access to the current "selections" of keyguard quick affordances, answering + * the question "which affordances should the keyguard show?" for the user associated with the + * System UI process. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class KeyguardQuickAffordanceLocalUserSelectionManager +@Inject +constructor( + @Application context: Context, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, + broadcastDispatcher: BroadcastDispatcher, +) : KeyguardQuickAffordanceSelectionManager { + + private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() + + private val userId: Flow<Int> = conflatedCallbackFlow { + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging(newUser, TAG) + } + } + + userTracker.addCallback(callback) { it.run() } + trySendWithFailureLogging(userTracker.userId, TAG) + + awaitClose { userTracker.removeCallback(callback) } + } + + private val defaults: Map<String, List<String>> by lazy { + context.resources + .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) + .associate { item -> + val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) + check(splitUp.size == 2) + val slotId = splitUp[0] + val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) + slotId to affordanceIds + } + } + + /** + * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an + * initial value. + */ + private val backupRestorationEvents: Flow<Unit> = + broadcastDispatcher.broadcastFlow( + filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), + flags = Context.RECEIVER_NOT_EXPORTED, + permission = BackupHelper.PERMISSION_SELF, + ) + + override val selections: Flow<Map<String, List<String>>> = + combine( + userId, + backupRestorationEvents.onStart { + // We emit an initial event to make sure that the combine emits at least once, + // even if we never get a Backup & Restore restoration event (which is the most + // common case anyway as restoration really only happens on initial device + // setup). + emit(Unit) + } + ) { _, _ -> } + .flatMapLatest { + conflatedCallbackFlow { + // We want to instantiate a new SharedPreferences instance each time either the + // user ID changes or we have a backup & restore restoration event. The reason + // is that our sharedPrefs instance needs to be replaced with a new one as it + // depends on the user ID and when the B&R job completes, the backing file is + // replaced but the existing instance still has a stale in-memory cache. + sharedPrefs = instantiateSharedPrefs() + + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + trySend(getSelections()) + } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + send(getSelections()) + + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } + } + + override fun getSelections(): Map<String, List<String>> { + val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } + val result = + slotKeys + .associate { key -> + val slotId = key.substring(KEY_PREFIX_SLOT.length) + val value = sharedPrefs.getString(key, null) + val affordanceIds = + if (!value.isNullOrEmpty()) { + value.split(AFFORDANCE_DELIMITER) + } else { + emptyList() + } + slotId to affordanceIds + } + .toMutableMap() + + // If the result map is missing keys, it means that the system has never set anything for + // those slots. This is where we need examine our defaults and see if there should be a + // default value for the affordances in the slot IDs that are missing from the result. + // + // Once the user makes any selection for a slot, even when they select "None", this class + // will persist a key for that slot ID. In the case of "None", it will have a value of the + // empty string. This is why this system works. + defaults.forEach { (slotId, affordanceIds) -> + if (!result.containsKey(slotId)) { + result[slotId] = affordanceIds + } + } + + return result + } + + override fun setSelections( + slotId: String, + affordanceIds: List<String>, + ) { + val key = "$KEY_PREFIX_SLOT$slotId" + val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) + sharedPrefs.edit().putString(key, value).apply() + } + + private fun instantiateSharedPrefs(): SharedPreferences { + return userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + } + + companion object { + private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" + const val FILE_NAME = "quick_affordance_selections" + private const val KEY_PREFIX_SLOT = "slot_" + private const val SLOT_AFFORDANCES_DELIMITER = ":" + private const val AFFORDANCE_DELIMITER = "," + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt new file mode 100644 index 000000000000..727a81391dc2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt @@ -0,0 +1,43 @@ +/* + * 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.quickaffordance + +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher + +interface KeyguardQuickAffordanceProviderClientFactory { + fun create(): KeyguardQuickAffordanceProviderClient +} + +class KeyguardQuickAffordanceProviderClientFactoryImpl +@Inject +constructor( + private val userTracker: UserTracker, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : KeyguardQuickAffordanceProviderClientFactory { + override fun create(): KeyguardQuickAffordanceProviderClient { + return KeyguardQuickAffordanceProviderClientImpl( + context = userTracker.userContext, + backgroundDispatcher = backgroundDispatcher, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt new file mode 100644 index 000000000000..8ffef25d3aae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt @@ -0,0 +1,129 @@ +/* + * 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.quickaffordance + +import android.content.Context +import android.os.UserHandle +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.settings.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +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.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * Manages and provides access to the current "selections" of keyguard quick affordances, answering + * the question "which affordances should the keyguard show?" for users associated with other System + * UI processes. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class KeyguardQuickAffordanceRemoteUserSelectionManager +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val userTracker: UserTracker, + private val clientFactory: KeyguardQuickAffordanceProviderClientFactory, + private val userHandle: UserHandle, +) : KeyguardQuickAffordanceSelectionManager { + + private val userId: Flow<Int> = conflatedCallbackFlow { + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging(newUser, TAG) + } + } + + userTracker.addCallback(callback) { it.run() } + trySendWithFailureLogging(userTracker.userId, TAG) + + awaitClose { userTracker.removeCallback(callback) } + } + + private val clientOrNull: StateFlow<KeyguardQuickAffordanceProviderClient?> = + userId + .distinctUntilChanged() + .map { selectedUserId -> + if (userHandle.isSystem && userHandle.identifier != selectedUserId) { + clientFactory.create() + } else { + null + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + private val _selections: StateFlow<Map<String, List<String>>> = + clientOrNull + .flatMapLatest { client -> + client?.observeSelections()?.map { selections -> + buildMap<String, List<String>> { + selections.forEach { selection -> + val slotId = selection.slotId + val affordanceIds = (get(slotId) ?: emptyList()).toMutableList() + affordanceIds.add(selection.affordanceId) + put(slotId, affordanceIds) + } + } + } + ?: emptyFlow() + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) + + override val selections: Flow<Map<String, List<String>>> = _selections + + override fun getSelections(): Map<String, List<String>> { + return _selections.value + } + + override fun setSelections(slotId: String, affordanceIds: List<String>) { + clientOrNull.value?.let { client -> + scope.launch { + client.deleteAllSelections(slotId = slotId) + affordanceIds.forEach { affordanceId -> + client.insertSelection(slotId = slotId, affordanceId = affordanceId) + } + } + } + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceMultiUserSelectionManager" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index b29cf45cc709..21fffede5f97 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -17,119 +17,22 @@ package com.android.systemui.keyguard.data.quickaffordance -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting -import com.android.systemui.R -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.settings.UserFileManager -import com.android.systemui.settings.UserTracker -import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest /** - * Manages and provides access to the current "selections" of keyguard quick affordances, answering - * the question "which affordances should the keyguard show?". + * Defines interface for classes that manage and provide access to the current "selections" of + * keyguard quick affordances, answering the question "which affordances should the keyguard show?". */ -@SysUISingleton -class KeyguardQuickAffordanceSelectionManager -@Inject -constructor( - @Application context: Context, - private val userFileManager: UserFileManager, - private val userTracker: UserTracker, -) { - - private val sharedPrefs: SharedPreferences - get() = - userFileManager.getSharedPreferences( - FILE_NAME, - Context.MODE_PRIVATE, - userTracker.userId, - ) - - private val userId: Flow<Int> = conflatedCallbackFlow { - val callback = - object : UserTracker.Callback { - override fun onUserChanged(newUser: Int, userContext: Context) { - trySendWithFailureLogging(newUser, TAG) - } - } - - userTracker.addCallback(callback) { it.run() } - trySendWithFailureLogging(userTracker.userId, TAG) - - awaitClose { userTracker.removeCallback(callback) } - } - private val defaults: Map<String, List<String>> by lazy { - context.resources - .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) - .associate { item -> - val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) - check(splitUp.size == 2) - val slotId = splitUp[0] - val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) - slotId to affordanceIds - } - } +interface KeyguardQuickAffordanceSelectionManager { /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ - val selections: Flow<Map<String, List<String>>> = - userId.flatMapLatest { - conflatedCallbackFlow { - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> - trySend(getSelections()) - } - - sharedPrefs.registerOnSharedPreferenceChangeListener(listener) - send(getSelections()) - - awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } - } - } + val selections: Flow<Map<String, List<String>>> /** * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in * descending priority order. */ - fun getSelections(): Map<String, List<String>> { - val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } - val result = - slotKeys - .associate { key -> - val slotId = key.substring(KEY_PREFIX_SLOT.length) - val value = sharedPrefs.getString(key, null) - val affordanceIds = - if (!value.isNullOrEmpty()) { - value.split(AFFORDANCE_DELIMITER) - } else { - emptyList() - } - slotId to affordanceIds - } - .toMutableMap() - - // If the result map is missing keys, it means that the system has never set anything for - // those slots. This is where we need examine our defaults and see if there should be a - // default value for the affordances in the slot IDs that are missing from the result. - // - // Once the user makes any selection for a slot, even when they select "None", this class - // will persist a key for that slot ID. In the case of "None", it will have a value of the - // empty string. This is why this system works. - defaults.forEach { (slotId, affordanceIds) -> - if (!result.containsKey(slotId)) { - result[slotId] = affordanceIds - } - } - - return result - } + fun getSelections(): Map<String, List<String>> /** * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance @@ -138,17 +41,9 @@ constructor( fun setSelections( slotId: String, affordanceIds: List<String>, - ) { - val key = "$KEY_PREFIX_SLOT$slotId" - val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) - sharedPrefs.edit().putString(key, value).apply() - } + ) companion object { - private const val TAG = "KeyguardQuickAffordanceSelectionManager" - @VisibleForTesting const val FILE_NAME = "quick_affordance_selections" - private const val KEY_PREFIX_SLOT = "slot_" - private const val SLOT_AFFORDANCES_DELIMITER = ":" - private const val AFFORDANCE_DELIMITER = "," + const val FILE_NAME = "quick_affordance_selections" } } 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 d95a1a726bf5..e3f5e90b2300 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 @@ -18,45 +18,93 @@ package com.android.systemui.keyguard.data.repository import android.content.Context +import android.os.UserHandle import com.android.systemui.Dumpable import com.android.systemui.R +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.settings.UserTracker import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +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.map import kotlinx.coroutines.flow.stateIn /** Abstracts access to application state related to keyguard quick affordances. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceRepository @Inject constructor( @Application private val appContext: Context, @Application private val scope: CoroutineScope, - private val selectionManager: KeyguardQuickAffordanceSelectionManager, + private val localUserSelectionManager: KeyguardQuickAffordanceLocalUserSelectionManager, + private val remoteUserSelectionManager: KeyguardQuickAffordanceRemoteUserSelectionManager, + private val userTracker: UserTracker, legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer, private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, dumpManager: DumpManager, + userHandle: UserHandle, ) { + private val userId: Flow<Int> = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging(newUser, TAG) + } + } + + userTracker.addCallback(callback) { it.run() } + trySendWithFailureLogging(userTracker.userId, TAG) + + awaitClose { userTracker.removeCallback(callback) } + } + + private val selectionManager: StateFlow<KeyguardQuickAffordanceSelectionManager> = + userId + .distinctUntilChanged() + .map { selectedUserId -> + if (userHandle.identifier == selectedUserId) { + localUserSelectionManager + } else { + remoteUserSelectionManager + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = localUserSelectionManager, + ) + /** * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the * given ID. The configs are sorted in descending priority order. */ val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> = - selectionManager.selections - .map { selectionsBySlotId -> - selectionsBySlotId.mapValues { (_, selections) -> - configs.filter { selections.contains(it.key) } + selectionManager + .flatMapLatest { selectionManager -> + selectionManager.selections.map { selectionsBySlotId -> + selectionsBySlotId.mapValues { (_, selections) -> + configs.filter { selections.contains(it.key) } + } } } .stateIn( @@ -99,7 +147,7 @@ constructor( * slot with the given ID. The configs are sorted in descending priority order. */ fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { - val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) + val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList()) return configs.filter { selections.contains(it.key) } } @@ -108,7 +156,7 @@ constructor( * are sorted in descending priority order. */ fun getSelections(): Map<String, List<String>> { - return selectionManager.getSelections() + return selectionManager.value.getSelections() } /** @@ -119,7 +167,7 @@ constructor( slotId: String, affordanceIds: List<String>, ) { - selectionManager.setSelections( + selectionManager.value.setSelections( slotId = slotId, affordanceIds = affordanceIds, ) @@ -188,6 +236,7 @@ constructor( } companion object { + private const val TAG = "KeyguardQuickAffordanceRepository" private const val SLOT_CONFIG_DELIMITER = ":" } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt new file mode 100644 index 000000000000..0e865cee0b76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt @@ -0,0 +1,44 @@ +/* + * 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.backup + +import android.app.backup.SharedPreferencesBackupHelper +import android.content.Context +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.settings.UserFileManagerImpl + +/** Handles backup & restore for keyguard quick affordances. */ +class KeyguardQuickAffordanceBackupHelper( + context: Context, + userId: Int, +) : + SharedPreferencesBackupHelper( + context, + if (UserFileManagerImpl.isPrimaryUser(userId)) { + KeyguardQuickAffordanceSelectionManager.FILE_NAME + } else { + UserFileManagerImpl.secondaryUserFile( + context = context, + fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME, + directoryName = UserFileManagerImpl.SHARED_PREFS, + userId = userId, + ) + .also { UserFileManagerImpl.ensureParentDirExists(it) } + .toString() + } + ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 2d94d760cb54..748c6e8b75b9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -34,8 +34,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject @@ -190,8 +190,6 @@ constructor( /** Returns affordance IDs indexed by slot ID, for all known slots. */ suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> { - check(isUsingRepository) - val slots = repository.get().getSlotPickerRepresentations() val selections = repository.get().getSelections() val affordanceById = @@ -312,8 +310,6 @@ constructor( suspend fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> { - check(isUsingRepository) - return repository.get().getAffordancePickerRepresentations() } 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 84a80744185e..2cf5fb98d07e 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 @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.res.ColorStateList +import android.hardware.biometrics.BiometricSourceType import android.os.Handler import android.os.Trace import android.os.UserHandle @@ -71,7 +72,7 @@ constructor( KeyguardUpdateMonitor.getCurrentUser() ) && !needsFullscreenBouncer() && - !keyguardUpdateMonitor.userNeedsStrongAuth() && + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) && !keyguardBypassController.bypassEnabled /** Runnable to show the primary bouncer. */ 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 3276b6dd9748..cbe512ff83ba 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 @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.binder +import android.graphics.drawable.Animatable2 import android.util.Size import android.util.TypedValue import android.view.View @@ -27,12 +28,11 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.LockIconViewController import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.animation.Interpolators +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel @@ -73,7 +73,8 @@ object KeyguardBottomAreaViewBinder { fun onConfigurationChanged() /** - * Returns whether the keyguard bottom area should be constrained to the top of the lock icon + * Returns whether the keyguard bottom area should be constrained to the top of the lock + * icon */ fun shouldConstrainToTopOfLockIcon(): Boolean } @@ -217,7 +218,7 @@ object KeyguardBottomAreaViewBinder { } override fun shouldConstrainToTopOfLockIcon(): Boolean = - viewModel.shouldConstrainToTopOfLockIcon() + viewModel.shouldConstrainToTopOfLockIcon() } } @@ -248,6 +249,27 @@ object KeyguardBottomAreaViewBinder { IconViewBinder.bind(viewModel.icon, view) + (view.drawable as? Animatable2)?.let { animatable -> + (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId -> + // Always start the animation (we do call stop() below, if we need to skip it). + animatable.start() + + if (view.tag != iconResourceId) { + // Here when we haven't run the animation on a previous update. + // + // Save the resource ID for next time, so we know not to re-animate the same + // animation again. + view.tag = iconResourceId + } else { + // Here when we've already done this animation on a previous update and want to + // skip directly to the final frame of the animation to avoid running it. + // + // By calling stop after start, we go to the final frame of the animation. + animatable.stop() + } + } + } + view.isActivated = viewModel.isActivated view.drawable.setTint( Utils.getColorAttrDefaultColor( diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt index c27bfa338df3..bb04b6b41a33 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt @@ -30,10 +30,11 @@ import kotlinx.coroutines.flow.Flow */ interface Diffable<T> { /** - * Finds the differences between [prevVal] and [this] and logs those diffs to [row]. + * Finds the differences between [prevVal] and this object and logs those diffs to [row]. * * Each implementer should determine which individual fields have changed between [prevVal] and - * [this], and only log the fields that have actually changed. This helps save buffer space. + * this object, and only log the fields that have actually changed. This helps save buffer + * space. * * For example, if: * - prevVal = Object(val1=100, val2=200, val3=300) @@ -42,6 +43,16 @@ interface Diffable<T> { * Then only the val3 change should be logged. */ fun logDiffs(prevVal: T, row: TableRowLogger) + + /** + * Logs all the relevant fields of this object to [row]. + * + * As opposed to [logDiffs], this method should log *all* fields. + * + * Implementation is optional. This method will only be used with [logDiffsForTable] in order to + * fully log the initial value of the flow. + */ + fun logFull(row: TableRowLogger) {} } /** @@ -57,8 +68,35 @@ fun <T : Diffable<T>> Flow<T>.logDiffsForTable( columnPrefix: String, initialValue: T, ): Flow<T> { - return this.pairwiseBy(initialValue) { prevVal, newVal -> + // Fully log the initial value to the table. + val getInitialValue = { + tableLogBuffer.logChange(columnPrefix) { row -> initialValue.logFull(row) } + initialValue + } + return this.pairwiseBy(getInitialValue) { prevVal: T, newVal: T -> tableLogBuffer.logDiffs(columnPrefix, prevVal, newVal) newVal } } + +/** + * Each time the boolean flow is updated with a new value that's different from the previous value, + * logs the new value to the given [tableLogBuffer]. + */ +fun Flow<Boolean>.logDiffsForTable( + tableLogBuffer: TableLogBuffer, + columnPrefix: String, + columnName: String, + initialValue: Boolean, +): Flow<Boolean> { + val initialValueFun = { + tableLogBuffer.logChange(columnPrefix, columnName, initialValue) + initialValue + } + return this.pairwiseBy(initialValueFun) { prevVal, newVal: Boolean -> + if (prevVal != newVal) { + tableLogBuffer.logChange(columnPrefix, columnName, newVal) + } + newVal + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt index 429637a0ee4d..9d0b833d26cc 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -16,10 +16,7 @@ package com.android.systemui.log.table -import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable -import com.android.systemui.dump.DumpManager -import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.plugins.util.RingBuffer import com.android.systemui.util.time.SystemClock import java.io.PrintWriter @@ -83,7 +80,7 @@ class TableLogBuffer( maxSize: Int, private val name: String, private val systemClock: SystemClock, -) { +) : Dumpable { init { if (maxSize <= 0) { throw IllegalArgumentException("maxSize must be > 0") @@ -104,6 +101,9 @@ class TableLogBuffer( * @param columnPrefix a prefix that will be applied to every column name that gets logged. This * ensures that all the columns related to the same state object will be grouped together in the * table. + * + * @throws IllegalArgumentException if [columnPrefix] or column name contain "|". "|" is used as + * the separator token for parsing, so it can't be present in any part of the column name. */ @Synchronized fun <T : Diffable<T>> logDiffs(columnPrefix: String, prevVal: T, newVal: T) { @@ -113,6 +113,25 @@ class TableLogBuffer( newVal.logDiffs(prevVal, row) } + /** + * Logs change(s) to the buffer using [rowInitializer]. + * + * @param rowInitializer a function that will be called immediately to store relevant data on + * the row. + */ + @Synchronized + fun logChange(columnPrefix: String, rowInitializer: (TableRowLogger) -> Unit) { + val row = tempRow + row.timestamp = systemClock.currentTimeMillis() + row.columnPrefix = columnPrefix + rowInitializer(row) + } + + /** Logs a boolean change. */ + fun logChange(prefix: String, columnName: String, value: Boolean) { + logChange(systemClock.currentTimeMillis(), prefix, columnName, value) + } + // Keep these individual [logChange] methods private (don't let clients give us their own // timestamps.) @@ -135,32 +154,31 @@ class TableLogBuffer( @Synchronized private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange { + verifyValidName(prefix, columnName) val tableChange = buffer.advance() tableChange.reset(timestamp, prefix, columnName) return tableChange } - /** - * Registers this buffer as dumpables in [dumpManager]. Must be called for the table to be - * dumped. - * - * This will be automatically called in [TableLogBufferFactory.create]. - */ - fun registerDumpables(dumpManager: DumpManager) { - dumpManager.registerNormalDumpable("$name-changes", changeDumpable) - dumpManager.registerNormalDumpable("$name-table", tableDumpable) + private fun verifyValidName(prefix: String, columnName: String) { + if (prefix.contains(SEPARATOR)) { + throw IllegalArgumentException("columnPrefix cannot contain $SEPARATOR but was $prefix") + } + if (columnName.contains(SEPARATOR)) { + throw IllegalArgumentException( + "columnName cannot contain $SEPARATOR but was $columnName" + ) + } } - private val changeDumpable = Dumpable { pw, _ -> dumpChanges(pw) } - private val tableDumpable = Dumpable { pw, _ -> dumpTable(pw) } - - /** Dumps the list of [TableChange] objects. */ @Synchronized - @VisibleForTesting - fun dumpChanges(pw: PrintWriter) { + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println(HEADER_PREFIX + name) + pw.println("version $VERSION") for (i in 0 until buffer.size) { buffer[i].dump(pw) } + pw.println(FOOTER_PREFIX + name) } /** Dumps an individual [TableChange]. */ @@ -170,70 +188,14 @@ class TableLogBuffer( } val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(timestamp) pw.print(formattedTimestamp) - pw.print(" ") + pw.print(SEPARATOR) pw.print(this.getName()) - pw.print("=") + pw.print(SEPARATOR) pw.print(this.getVal()) pw.println() } /** - * Coalesces all the [TableChange] objects into a table of values of time and dumps the table. - */ - // TODO(b/259454430): Since this is an expensive process, it could cause the bug report dump to - // fail and/or not dump anything else. We should move this processing to ABT (Android Bug - // Tool), where we have unlimited time to process. - @Synchronized - @VisibleForTesting - fun dumpTable(pw: PrintWriter) { - val messages = buffer.iterator().asSequence().toList() - - if (messages.isEmpty()) { - return - } - - // Step 1: Create list of column headers - val headerSet = mutableSetOf<String>() - messages.forEach { headerSet.add(it.getName()) } - val headers: MutableList<String> = headerSet.toList().sorted().toMutableList() - headers.add(0, "timestamp") - - // Step 2: Create a list with the current values for each column. Will be updated with each - // change. - val currentRow: MutableList<String> = MutableList(headers.size) { DEFAULT_COLUMN_VALUE } - - // Step 3: For each message, make the correct update to [currentRow] and save it to [rows]. - val columnIndices: Map<String, Int> = - headers.mapIndexed { index, headerName -> headerName to index }.toMap() - val allRows = mutableListOf<List<String>>() - - messages.forEach { - if (!it.hasData()) { - return@forEach - } - - val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(it.timestamp) - if (formattedTimestamp != currentRow[0]) { - // The timestamp has updated, so save the previous row and continue to the next row - allRows.add(currentRow.toList()) - currentRow[0] = formattedTimestamp - } - val columnIndex = columnIndices[it.getName()]!! - currentRow[columnIndex] = it.getVal() - } - // Add the last row - allRows.add(currentRow.toList()) - - // Step 4: Dump the rows - DumpsysTableLogger( - name, - headers, - allRows, - ) - .printTableData(pw) - } - - /** * A private implementation of [TableRowLogger]. * * Used so that external clients can't modify [timestamp]. @@ -261,4 +223,8 @@ class TableLogBuffer( } val TABLE_LOG_DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) -private const val DEFAULT_COLUMN_VALUE = "UNKNOWN" + +private const val HEADER_PREFIX = "SystemUI StateChangeTableSection START: " +private const val FOOTER_PREFIX = "SystemUI StateChangeTableSection END: " +private const val SEPARATOR = "|" +private const val VERSION = "1" 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 f1f906f46d2d..7a90a7470cd2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -34,7 +34,7 @@ constructor( maxSize: Int, ): TableLogBuffer { val tableBuffer = TableLogBuffer(adjustMaxSize(maxSize), name, systemClock) - tableBuffer.registerDumpables(dumpManager) + dumpManager.registerNormalDumpable(name, tableBuffer) return tableBuffer } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index 4891297dbcf9..2d10b823f784 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -32,10 +32,12 @@ import com.android.systemui.Dumpable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT +import com.android.systemui.settings.UserTracker import com.android.systemui.tuner.TunerService import com.android.systemui.util.Utils import com.android.systemui.util.time.SystemClock @@ -55,6 +57,8 @@ class MediaResumeListener constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, + @Main private val mainExecutor: Executor, @Background private val backgroundExecutor: Executor, private val tunerService: TunerService, private val mediaBrowserFactory: ResumeMediaBrowserFactory, @@ -77,18 +81,26 @@ constructor( private var currentUserId: Int = context.userId @VisibleForTesting - val userChangeReceiver = + val userUnlockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_USER_UNLOCKED == intent.action) { - loadMediaResumptionControls() - } else if (Intent.ACTION_USER_SWITCHED == intent.action) { - currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) - loadSavedComponents() + val userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (userId == currentUserId) { + loadMediaResumptionControls() + } } } } + private val userTrackerCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + currentUserId = newUser + loadSavedComponents() + } + } + private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() { override fun addTrack( @@ -126,13 +138,13 @@ constructor( dumpManager.registerDumpable(TAG, this) val unlockFilter = IntentFilter() unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) - unlockFilter.addAction(Intent.ACTION_USER_SWITCHED) broadcastDispatcher.registerReceiver( - userChangeReceiver, + userUnlockReceiver, unlockFilter, null, UserHandle.ALL ) + userTracker.addCallback(userTrackerCallback, mainExecutor) loadSavedComponents() } } 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 8aaee81a57dd..1fdbc99333cb 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 @@ -184,6 +184,7 @@ constructor( private val configListener = object : ConfigurationController.ConfigurationListener { + override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may // occur @@ -199,6 +200,7 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { if (newConfig == null) return isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL + updatePlayers(recreateMedia = true) } override fun onUiModeChanged() { @@ -635,7 +637,7 @@ constructor( val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() existingSmartspaceMediaKey?.let { val removedPlayer = - MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true) + removePlayer(existingSmartspaceMediaKey, dismissMediaData = false) removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) } @@ -685,7 +687,7 @@ constructor( key: String, dismissMediaData: Boolean = true, dismissRecommendation: Boolean = true - ) { + ): MediaControlPanel? { if (key == MediaPlayerData.smartspaceMediaKey()) { MediaPlayerData.smartspaceMediaData?.let { logger.logRecommendationRemoved(it.packageName, it.instanceId) @@ -693,7 +695,7 @@ constructor( } val removed = MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation) - removed?.apply { + return removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaContent.removeView(removed.mediaViewHolder?.player) mediaContent.removeView(removed.recommendationViewHolder?.recommendations) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index b91039d8d928..d762b39fa1af 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -52,13 +52,12 @@ import static com.android.systemui.statusbar.phone.CentralSurfaces.dumpBarTransi import static com.android.systemui.util.Utils.isGesturalModeOnDefaultDisplay; import android.annotation.IdRes; +import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.StatusBarManager; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.PixelFormat; @@ -114,7 +113,6 @@ import com.android.internal.view.AppearanceRegion; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; @@ -132,6 +130,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.settings.UserContextProvider; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.recents.utilities.Utilities; @@ -202,7 +201,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final NotificationRemoteInputManager mNotificationRemoteInputManager; private final OverviewProxyService mOverviewProxyService; private final NavigationModeController mNavigationModeController; - private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; private final CommandQueue mCommandQueue; private final Optional<Pip> mPipOptional; private final Optional<Recents> mRecentsOptional; @@ -504,7 +503,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, SysUiState sysUiFlagsContainer, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, CommandQueue commandQueue, Optional<Pip> pipOptional, Optional<Recents> recentsOptional, @@ -547,7 +546,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mNotificationRemoteInputManager = notificationRemoteInputManager; mOverviewProxyService = overviewProxyService; mNavigationModeController = navigationModeController; - mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; mCommandQueue = commandQueue; mPipOptional = pipOptional; mRecentsOptional = recentsOptional; @@ -729,9 +728,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements prepareNavigationBarView(); checkNavBarModes(); - IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiverWithHandler(mBroadcastReceiver, filter, - Handler.getMain(), UserHandle.ALL); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); notifyNavigationBarScreenOn(); @@ -782,7 +779,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.setUpdateActiveTouchRegionsCallback(null); getBarTransitions().destroy(); mOverviewProxyService.removeCallback(mOverviewProxyListener); - mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); + mUserTracker.removeCallback(mUserChangedCallback); mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); if (mOrientationHandle != null) { resetSecondaryHandle(); @@ -1674,21 +1671,14 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // TODO(193941146): Currently unregistering a receiver through BroadcastDispatcher is - // async, but we've already cleared the fields. Just return early in this case. - if (mView == null) { - return; - } - String action = intent.getAction(); - if (Intent.ACTION_USER_SWITCHED.equals(action)) { - // The accessibility settings may be different for the new user - updateAccessibilityStateFlags(); - } - } - }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + // The accessibility settings may be different for the new user + updateAccessibilityStateFlags(); + } + }; @VisibleForTesting int getNavigationIconHints() { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 1da866efc08d..5a1ad96da7a9 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -39,6 +39,8 @@ import android.text.format.DateUtils; import android.util.Log; import android.util.Slog; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.fuelgauge.Estimate; import com.android.settingslib.utils.ThreadUtils; @@ -47,6 +49,7 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -80,6 +83,7 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { private final PowerManager mPowerManager; private final WarningsUI mWarnings; private final WakefulnessLifecycle mWakefulnessLifecycle; + private final UserTracker mUserTracker; private InattentiveSleepWarningView mOverlayView; private final Configuration mLastConfiguration = new Configuration(); private int mPlugType = 0; @@ -122,12 +126,21 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { } }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mWarnings.userSwitched(); + } + }; + @Inject public PowerUI(Context context, BroadcastDispatcher broadcastDispatcher, CommandQueue commandQueue, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, WarningsUI warningsUI, EnhancedEstimates enhancedEstimates, WakefulnessLifecycle wakefulnessLifecycle, - PowerManager powerManager) { + PowerManager powerManager, + UserTracker userTracker) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mCommandQueue = commandQueue; @@ -136,6 +149,7 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { mEnhancedEstimates = enhancedEstimates; mPowerManager = powerManager; mWakefulnessLifecycle = wakefulnessLifecycle; + mUserTracker = userTracker; } public void start() { @@ -154,6 +168,7 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { false, obs, UserHandle.USER_ALL); updateBatteryWarningLevels(); mReceiver.init(); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); // Check to see if we need to let the user know that the phone previously shut down due @@ -250,7 +265,6 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { IntentFilter filter = new IntentFilter(); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); filter.addAction(Intent.ACTION_BATTERY_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler); // Force get initial values. Relying on Sticky behavior until API for getting info. if (!mHasReceivedBattery) { @@ -332,8 +346,6 @@ public class PowerUI implements CoreStartable, CommandQueue.Callbacks { plugged, bucket); }); - } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { - mWarnings.userSwitched(); } else { Slog.w(TAG, "unknown intent: " + intent); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index 2ee5f05549cf..645b1256e5f1 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -51,10 +51,13 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.util.leak.RotationUtils; @@ -76,6 +79,7 @@ public class ScreenPinningRequest implements View.OnClickListener, private final AccessibilityManager mAccessibilityService; private final WindowManager mWindowManager; private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; private RequestWindowView mRequestWindow; private int mNavBarMode; @@ -83,12 +87,21 @@ public class ScreenPinningRequest implements View.OnClickListener, /** ID of task to be pinned or locked. */ private int taskId; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + clearPrompt(); + } + }; + @Inject public ScreenPinningRequest( Context context, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, NavigationModeController navigationModeController, - BroadcastDispatcher broadcastDispatcher) { + BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker) { mContext = context; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; mAccessibilityService = (AccessibilityManager) @@ -97,6 +110,7 @@ public class ScreenPinningRequest implements View.OnClickListener, mContext.getSystemService(Context.WINDOW_SERVICE); mNavBarMode = navigationModeController.addListener(this); mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; } public void clearPrompt() { @@ -228,9 +242,9 @@ public class ScreenPinningRequest implements View.OnClickListener, } IntentFilter filter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_SCREEN_OFF); mBroadcastDispatcher.registerReceiver(mReceiver, filter); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); } private void inflateView(int rotation) { @@ -358,6 +372,7 @@ public class ScreenPinningRequest implements View.OnClickListener, @Override public void onDetachedFromWindow() { mBroadcastDispatcher.unregisterReceiver(mReceiver); + mUserTracker.removeCallback(mUserChangedCallback); } protected void onConfigurationChanged() { @@ -388,8 +403,7 @@ public class ScreenPinningRequest implements View.OnClickListener, public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { post(mUpdateLayoutRunnable); - } else if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED) - || intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { + } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { clearPrompt(); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index ce4e0ecee914..b8684ee30b9a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -33,13 +33,16 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +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.settings.UserContextProvider; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.CallbackController; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -55,8 +58,10 @@ public class RecordingController private boolean mIsRecording; private PendingIntent mStopIntent; private CountDownTimer mCountDownTimer = null; - private BroadcastDispatcher mBroadcastDispatcher; - private UserContextProvider mUserContextProvider; + private final Executor mMainExecutor; + private final BroadcastDispatcher mBroadcastDispatcher; + private final UserContextProvider mUserContextProvider; + private final UserTracker mUserTracker; protected static final String INTENT_UPDATE_STATE = "com.android.systemui.screenrecord.UPDATE_STATE"; @@ -66,12 +71,13 @@ public class RecordingController new CopyOnWriteArrayList<>(); @VisibleForTesting - protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - stopRecording(); - } - }; + final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + stopRecording(); + } + }; @VisibleForTesting protected final BroadcastReceiver mStateChangeReceiver = new BroadcastReceiver() { @@ -92,10 +98,14 @@ public class RecordingController * Create a new RecordingController */ @Inject - public RecordingController(BroadcastDispatcher broadcastDispatcher, - UserContextProvider userContextProvider) { + public RecordingController(@Main Executor mainExecutor, + BroadcastDispatcher broadcastDispatcher, + UserContextProvider userContextProvider, + UserTracker userTracker) { + mMainExecutor = mainExecutor; mBroadcastDispatcher = broadcastDispatcher; mUserContextProvider = userContextProvider; + mUserTracker = userTracker; } /** Create a dialog to show screen recording options to the user. */ @@ -139,9 +149,7 @@ public class RecordingController } try { startIntent.send(); - IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null, - UserHandle.ALL); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE); mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null, @@ -211,7 +219,7 @@ public class RecordingController public synchronized void updateState(boolean isRecording) { if (!isRecording && mIsRecording) { // Unregister receivers if we have stopped recording - mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); + mUserTracker.removeCallback(mUserChangedCallback); mBroadcastDispatcher.unregisterReceiver(mStateChangeReceiver); } mIsRecording = isRecording; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 10d31ea2d277..a6447a5bf500 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -84,6 +84,8 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.widget.Toast; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import android.window.WindowContext; import androidx.concurrent.futures.CallbackToFutureAdapter; @@ -279,6 +281,13 @@ public class ScreenshotController { private final ActionIntentExecutor mActionExecutor; private final UserManager mUserManager; + private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { + if (DEBUG_INPUT) { + Log.d(TAG, "Predictive Back callback dispatched"); + } + respondToBack(); + }; + private ScreenshotView mScreenshotView; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; @@ -465,6 +474,10 @@ public class ScreenshotController { } } + private void respondToBack() { + dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); + } + /** * Update resources on configuration change. Reinflate for theme/color changes. */ @@ -476,6 +489,26 @@ public class ScreenshotController { // Inflate the screenshot layout mScreenshotView = (ScreenshotView) LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); + mScreenshotView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + if (DEBUG_INPUT) { + Log.d(TAG, "Registering Predictive Back callback"); + } + mScreenshotView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback); + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + if (DEBUG_INPUT) { + Log.d(TAG, "Unregistering Predictive Back callback"); + } + mScreenshotView.findOnBackInvokedDispatcher() + .unregisterOnBackInvokedCallback(mOnBackInvokedCallback); + } + }); mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() { @Override public void onUserInteraction() { @@ -503,7 +536,7 @@ public class ScreenshotController { if (DEBUG_INPUT) { Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK"); } - dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); + respondToBack(); return true; } return false; @@ -546,9 +579,16 @@ public class ScreenshotController { private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) { - withWindowAttached(() -> + withWindowAttached(() -> { + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY) + && mUserManager.isManagedProfile(owner.getIdentifier())) { + mScreenshotView.announceForAccessibility(mContext.getResources().getString( + R.string.screenshot_saving_work_profile_title)); + } else { mScreenshotView.announceForAccessibility( - mContext.getResources().getString(R.string.screenshot_saving_title))); + mContext.getResources().getString(R.string.screenshot_saving_title)); + } + }); mScreenshotView.reset(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 7641554ede3d..fae938d542f1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -825,12 +825,23 @@ public class ScreenshotView extends FrameLayout implements } }); if (mQuickShareChip != null) { - mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent, - () -> { - mUiEventLogger.log( - ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0, mPackageName); - animateDismissal(); - }); + if (imageData.quickShareAction != null) { + mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent, + () -> { + mUiEventLogger.log( + ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0, + mPackageName); + animateDismissal(); + }); + } else { + // hide chip and unset pending interaction if necessary, since we don't actually + // have a useable quick share intent + Log.wtf(TAG, "Showed quick share chip, but quick share intent was null"); + if (mPendingInteraction == PendingInteraction.QUICK_SHARE) { + mPendingInteraction = null; + } + mQuickShareChip.setVisibility(GONE); + } } if (mPendingInteraction != null) { diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt index d450afa59c7d..bfba6dfddfac 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt @@ -35,12 +35,14 @@ import java.io.File import javax.inject.Inject /** - * Implementation for retrieving file paths for file storage of system and secondary users. - * Files lie in {File Directory}/UserFileManager/{User Id} for secondary user. - * For system user, we use the conventional {File Directory} + * Implementation for retrieving file paths for file storage of system and secondary users. Files + * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the + * conventional {File Directory} */ @SysUISingleton -class UserFileManagerImpl @Inject constructor( +class UserFileManagerImpl +@Inject +constructor( // Context of system process and system user. private val context: Context, val userManager: UserManager, @@ -49,80 +51,114 @@ class UserFileManagerImpl @Inject constructor( ) : UserFileManager, CoreStartable { companion object { private const val FILES = "files" - @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs" + const val SHARED_PREFS = "shared_prefs" @VisibleForTesting internal const val ID = "UserFileManager" - } - private val broadcastReceiver = object : BroadcastReceiver() { + /** Returns `true` if the given user ID is that for the primary/system user. */ + fun isPrimaryUser(userId: Int): Boolean { + return UserHandle(userId).isSystem + } + /** - * Listen to Intent.ACTION_USER_REMOVED to clear user data. + * Returns a [File] pointing to the correct path for a secondary user ID. + * + * Note that there is no check for the type of user. This should only be called for + * secondary users, never for the system user. For that, make sure to call [isPrimaryUser]. + * + * Note also that there is no guarantee that the parent directory structure for the file + * exists on disk. For that, call [ensureParentDirExists]. + * + * @param context The context + * @param fileName The name of the file + * @param directoryName The name of the directory that would contain the file + * @param userId The ID of the user to build a file path for */ - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_USER_REMOVED) { - clearDeletedUserData() + fun secondaryUserFile( + context: Context, + fileName: String, + directoryName: String, + userId: Int, + ): File { + return Environment.buildPath( + context.filesDir, + ID, + userId.toString(), + directoryName, + fileName, + ) + } + + /** + * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs + * recursively. + */ + fun ensureParentDirExists(file: File) { + val parent = file.parentFile + if (!parent.exists()) { + if (!parent.mkdirs()) { + Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") + } } } } - /** - * Poll for user-specific directories to delete upon start up. - */ + private val broadcastReceiver = + object : BroadcastReceiver() { + /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */ + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_REMOVED) { + clearDeletedUserData() + } + } + } + + /** Poll for user-specific directories to delete upon start up. */ override fun start() { clearDeletedUserData() - val filter = IntentFilter().apply { - addAction(Intent.ACTION_USER_REMOVED) - } + val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) } broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor) } - /** - * Return the file based on current user. - */ + /** Return the file based on current user. */ override fun getFile(fileName: String, userId: Int): File { - return if (UserHandle(userId).isSystem) { - Environment.buildPath( - context.filesDir, - fileName - ) + return if (isPrimaryUser(userId)) { + Environment.buildPath(context.filesDir, fileName) } else { - val secondaryFile = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - FILES, - fileName - ) + val secondaryFile = + secondaryUserFile( + context = context, + userId = userId, + directoryName = FILES, + fileName = fileName, + ) ensureParentDirExists(secondaryFile) secondaryFile } } - /** - * Get shared preferences from user. - */ + /** Get shared preferences from user. */ override fun getSharedPreferences( fileName: String, @Context.PreferencesMode mode: Int, userId: Int ): SharedPreferences { - if (UserHandle(userId).isSystem) { + if (isPrimaryUser(userId)) { return context.getSharedPreferences(fileName, mode) } - val secondaryUserDir = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - SHARED_PREFS, - fileName - ) + + val secondaryUserDir = + secondaryUserFile( + context = context, + fileName = fileName, + directoryName = SHARED_PREFS, + userId = userId, + ) ensureParentDirExists(secondaryUserDir) return context.getSharedPreferences(secondaryUserDir, mode) } - /** - * Remove dirs for deleted users. - */ + /** Remove dirs for deleted users. */ @VisibleForTesting internal fun clearDeletedUserData() { backgroundExecutor.execute { @@ -133,10 +169,11 @@ class UserFileManagerImpl @Inject constructor( dirsToDelete.forEach { dir -> try { - val dirToDelete = Environment.buildPath( - file, - dir, - ) + val dirToDelete = + Environment.buildPath( + file, + dir, + ) dirToDelete.deleteRecursively() } catch (e: Exception) { Log.e(ID, "Deletion failed.", e) @@ -144,18 +181,4 @@ class UserFileManagerImpl @Inject constructor( } } } - - /** - * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs - * recursively. - */ - @VisibleForTesting - internal fun ensureParentDirExists(file: File) { - val parent = file.parentFile - if (!parent.exists()) { - if (!parent.mkdirs()) { - Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index e68182ef85c9..f52ace7138f2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -393,6 +393,9 @@ public final class NotificationPanelViewController implements Dumpable { private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private int mQsTrackingPointer; private VelocityTracker mQsVelocityTracker; + private TrackingStartedListener mTrackingStartedListener; + private OpenCloseListener mOpenCloseListener; + private GestureRecorder mGestureRecorder; private boolean mQsTracking; /** Whether the ongoing gesture might both trigger the expansion in both the view and QS. */ private boolean mConflictingQsExpansionGesture; @@ -1362,6 +1365,14 @@ public final class NotificationPanelViewController implements Dumpable { mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea); } + void setOpenCloseListener(OpenCloseListener openCloseListener) { + mOpenCloseListener = openCloseListener; + } + + void setTrackingStartedListener(TrackingStartedListener trackingStartedListener) { + mTrackingStartedListener = trackingStartedListener; + } + private void updateGestureExclusionRect() { Rect exclusionRect = calculateGestureExclusionRect(); mView.setSystemGestureExclusionRects(exclusionRect.isEmpty() ? Collections.emptyList() @@ -1936,9 +1947,9 @@ public final class NotificationPanelViewController implements Dumpable { } private void fling(float vel) { - GestureRecorder gr = mCentralSurfaces.getGestureRecorder(); - if (gr != null) { - gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); + if (mGestureRecorder != null) { + mGestureRecorder.tag("fling " + ((vel > 0) ? "open" : "closed"), + "notifications,v=" + vel); } fling(vel, true, 1.0f /* collapseSpeedUpFactor */, false); } @@ -2072,6 +2083,14 @@ public final class NotificationPanelViewController implements Dumpable { mInitialTouchX = x; initVelocityTracker(); trackMovement(event); + float qsExpansionFraction = computeQsExpansionFraction(); + // Intercept the touch if QS is between fully collapsed and fully expanded state + if (!mSplitShadeEnabled + && qsExpansionFraction > 0.0 && qsExpansionFraction < 1.0) { + mShadeLog.logMotionEvent(event, + "onQsIntercept: down action, QS partially expanded/collapsed"); + return true; + } if (mKeyguardShowing && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { // Dragging down on the lockscreen statusbar should prohibit other interactions @@ -2324,6 +2343,14 @@ public final class NotificationPanelViewController implements Dumpable { if (!isFullyCollapsed()) { handleQsDown(event); } + // defer touches on QQS to shade while shade is collapsing. Added margin for error + // as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS. + if (!mSplitShadeEnabled + && computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) { + mShadeLog.logMotionEvent(event, + "handleQsTouch: QQS touched while shade collapsing"); + mQsTracking = false; + } if (!mQsExpandImmediate && mQsTracking) { onQsTouch(event); if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) { @@ -2564,7 +2591,6 @@ public final class NotificationPanelViewController implements Dumpable { // Reset scroll position and apply that position to the expanded height. float height = mQsExpansionHeight; setQsExpansionHeight(height); - updateExpandedHeightToMaxHeight(); mNotificationStackScrollLayoutController.checkSnoozeLeavebehind(); // When expanding QS, let's authenticate the user if possible, @@ -3711,7 +3737,7 @@ public final class NotificationPanelViewController implements Dumpable { mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen()); endClosing(); mTracking = true; - mCentralSurfaces.onTrackingStarted(); + mTrackingStartedListener.onTrackingStarted(); notifyExpandingStarted(); updatePanelExpansionAndVisibility(); mScrimController.onTrackingStarted(); @@ -3945,7 +3971,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void onClosingFinished() { - mCentralSurfaces.onClosingFinished(); + mOpenCloseListener.onClosingFinished(); setClosingWithAlphaFadeout(false); mMediaHierarchyManager.closeGuts(); } @@ -4504,11 +4530,13 @@ public final class NotificationPanelViewController implements Dumpable { */ public void initDependencies( CentralSurfaces centralSurfaces, + GestureRecorder recorder, Runnable hideExpandedRunnable, NotificationShelfController notificationShelfController) { // TODO(b/254859580): this can be injected. mCentralSurfaces = centralSurfaces; + mGestureRecorder = recorder; mHideExpandedRunnable = hideExpandedRunnable; mNotificationStackScrollLayoutController.setShelfController(notificationShelfController); mNotificationShelfController = notificationShelfController; @@ -4590,20 +4618,20 @@ public final class NotificationPanelViewController implements Dumpable { return false; } - // If the view that would receive the touch is disabled, just have status bar - // eat the gesture. - if (event.getAction() == MotionEvent.ACTION_DOWN && !mView.isEnabled()) { - Log.v(TAG, - String.format( - "onTouchForwardedFromStatusBar: " - + "panel view disabled, eating touch at (%d,%d)", - (int) event.getX(), - (int) event.getY() - ) - ); - return true; + if (event.getAction() == MotionEvent.ACTION_DOWN) { + // If the view that would receive the touch is disabled, just have status + // bar eat the gesture. + if (!mView.isEnabled()) { + mShadeLog.logMotionEvent(event, + "onTouchForwardedFromStatusBar: panel view disabled"); + return true; + } + if (isFullyCollapsed() && event.getY() < 1f) { + // b/235889526 Eat events on the top edge of the phone when collapsed + mShadeLog.logMotionEvent(event, "top edge touch ignored"); + return true; + } } - return mView.dispatchTouchEvent(event); } }; @@ -5757,7 +5785,7 @@ public final class NotificationPanelViewController implements Dumpable { if (mSplitShadeEnabled && !isOnKeyguard()) { setQsExpandImmediate(true); } - mCentralSurfaces.makeExpandedVisible(false); + mOpenCloseListener.onOpenStarted(); } if (state == STATE_CLOSED) { setQsExpandImmediate(false); @@ -6240,4 +6268,17 @@ public final class NotificationPanelViewController implements Dumpable { return super.performAccessibilityAction(host, action, args); } } + + /** Listens for when touch tracking begins. */ + interface TrackingStartedListener { + void onTrackingStarted(); + } + + /** Listens for when shade begins opening of finishes closing. */ + interface OpenCloseListener { + /** Called when the shade finishes closing. */ + void onClosingFinished(); + /** Called when the shade starts opening. */ + void onOpenStarted(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java index aa610bdcc90e..a41a15d4e2a2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java @@ -16,6 +16,9 @@ package com.android.systemui.shade; +import android.view.MotionEvent; + +import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -29,31 +32,32 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; */ public interface ShadeController { - /** - * Make our window larger and the panel expanded - */ - void instantExpandNotificationsPanel(); + /** Make our window larger and the shade expanded */ + void instantExpandShade(); + + /** Collapse the shade instantly with no animation. */ + void instantCollapseShade(); + + /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + void animateCollapseShade(); + + /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + void animateCollapseShade(int flags); - /** See {@link #animateCollapsePanels(int, boolean)}. */ - void animateCollapsePanels(); + /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + void animateCollapseShadeForced(); - /** See {@link #animateCollapsePanels(int, boolean)}. */ - void animateCollapsePanels(int flags); + /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */ + void animateCollapseShadeDelayed(); /** * Collapse the shade animated, showing the bouncer when on {@link StatusBarState#KEYGUARD} or - * dismissing {@link CentralSurfaces} when on {@link StatusBarState#SHADE}. + * dismissing status bar when on {@link StatusBarState#SHADE}. */ - void animateCollapsePanels(int flags, boolean force); - - /** See {@link #animateCollapsePanels(int, boolean)}. */ - void animateCollapsePanels(int flags, boolean force, boolean delayed); - - /** See {@link #animateCollapsePanels(int, boolean)}. */ void animateCollapsePanels(int flags, boolean force, boolean delayed, float speedUpFactor); /** - * If the notifications panel is not fully expanded, collapse it animated. + * If the shade is not fully expanded, collapse it animated. * * @return Seems to always return false */ @@ -77,9 +81,7 @@ public interface ShadeController { */ void addPostCollapseAction(Runnable action); - /** - * Run all of the runnables added by {@link #addPostCollapseAction}. - */ + /** Run all of the runnables added by {@link #addPostCollapseAction}. */ void runPostCollapseRunnables(); /** @@ -87,13 +89,51 @@ public interface ShadeController { * * @return true if the shade was open, else false */ - boolean collapsePanel(); + boolean collapseShade(); /** - * If animate is true, does the same as {@link #collapsePanel()}. Otherwise, instantly collapse - * the panel. Post collapse runnables will be executed + * If animate is true, does the same as {@link #collapseShade()}. Otherwise, instantly collapse + * the shade. Post collapse runnables will be executed * * @param animate true to animate the collapse, false for instantaneous collapse */ - void collapsePanel(boolean animate); + void collapseShade(boolean animate); + + /** Makes shade expanded but not visible. */ + void makeExpandedInvisible(); + + /** Makes shade expanded and visible. */ + void makeExpandedVisible(boolean force); + + /** Returns whether the shade is expanded and visible. */ + boolean isExpandedVisible(); + + /** Handle status bar touch event. */ + void onStatusBarTouch(MotionEvent event); + + /** Called when the shade finishes collapsing. */ + void onClosingFinished(); + + /** Sets the listener for when the visibility of the shade changes. */ + void setVisibilityListener(ShadeVisibilityListener listener); + + /** */ + void setNotificationPresenter(NotificationPresenter presenter); + + /** */ + void setNotificationShadeWindowViewController( + NotificationShadeWindowViewController notificationShadeWindowViewController); + + /** */ + void setNotificationPanelViewController( + NotificationPanelViewController notificationPanelViewController); + + /** Listens for shade visibility changes. */ + interface ShadeVisibilityListener { + /** Called when the visibility of the shade changes. */ + void visibilityChanged(boolean visible); + + /** Called when shade expanded and visible state changed. */ + void expandedVisibleChanged(boolean expandedVisible); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index d783293b95d4..638e74883f23 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -16,9 +16,12 @@ package com.android.systemui.shade; +import android.content.ComponentCallbacks2; import android.util.Log; +import android.view.MotionEvent; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; @@ -27,11 +30,12 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.window.StatusBarWindowController; import java.util.ArrayList; -import java.util.Optional; import javax.inject.Inject; @@ -39,68 +43,81 @@ import dagger.Lazy; /** An implementation of {@link ShadeController}. */ @SysUISingleton -public class ShadeControllerImpl implements ShadeController { +public final class ShadeControllerImpl implements ShadeController { private static final String TAG = "ShadeControllerImpl"; private static final boolean SPEW = false; + private final int mDisplayId; + private final CommandQueue mCommandQueue; + private final KeyguardStateController mKeyguardStateController; + private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarStateController mStatusBarStateController; - protected final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final int mDisplayId; - protected final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; + private final StatusBarWindowController mStatusBarWindowController; + private final Lazy<AssistManager> mAssistManagerLazy; + private final Lazy<NotificationGutsManager> mGutsManager; private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); + private boolean mExpandedVisible; + + private NotificationPanelViewController mNotificationPanelViewController; + private NotificationPresenter mPresenter; + private NotificationShadeWindowViewController mNotificationShadeWindowViewController; + private ShadeVisibilityListener mShadeVisibilityListener; + @Inject public ShadeControllerImpl( CommandQueue commandQueue, + KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, - NotificationShadeWindowController notificationShadeWindowController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, + StatusBarWindowController statusBarWindowController, + NotificationShadeWindowController notificationShadeWindowController, WindowManager windowManager, - Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, - Lazy<AssistManager> assistManagerLazy + Lazy<AssistManager> assistManagerLazy, + Lazy<NotificationGutsManager> gutsManager ) { mCommandQueue = commandQueue; mStatusBarStateController = statusBarStateController; + mStatusBarWindowController = statusBarWindowController; + mGutsManager = gutsManager; mNotificationShadeWindowController = notificationShadeWindowController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mDisplayId = windowManager.getDefaultDisplay().getDisplayId(); - // TODO: Remove circular reference to CentralSurfaces when possible. - mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; + mKeyguardStateController = keyguardStateController; mAssistManagerLazy = assistManagerLazy; } @Override - public void instantExpandNotificationsPanel() { + public void instantExpandShade() { // Make our window larger and the panel expanded. - getCentralSurfaces().makeExpandedVisible(true /* force */); - getNotificationPanelViewController().expand(false /* animate */); + makeExpandedVisible(true /* force */); + mNotificationPanelViewController.expand(false /* animate */); mCommandQueue.recomputeDisableFlags(mDisplayId, false /* animate */); } @Override - public void animateCollapsePanels() { - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); + public void animateCollapseShade() { + animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); } @Override - public void animateCollapsePanels(int flags) { - animateCollapsePanels(flags, false /* force */, false /* delayed */, - 1.0f /* speedUpFactor */); + public void animateCollapseShade(int flags) { + animateCollapsePanels(flags, false, false, 1.0f); } @Override - public void animateCollapsePanels(int flags, boolean force) { - animateCollapsePanels(flags, force, false /* delayed */, 1.0f /* speedUpFactor */); + public void animateCollapseShadeForced() { + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true, false, 1.0f); } @Override - public void animateCollapsePanels(int flags, boolean force, boolean delayed) { - animateCollapsePanels(flags, force, delayed, 1.0f /* speedUpFactor */); + public void animateCollapseShadeDelayed() { + animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true, true, 1.0f); } @Override @@ -111,34 +128,26 @@ public class ShadeControllerImpl implements ShadeController { return; } if (SPEW) { - Log.d(TAG, "animateCollapse():" - + " mExpandedVisible=" + getCentralSurfaces().isExpandedVisible() - + " flags=" + flags); + Log.d(TAG, + "animateCollapse(): mExpandedVisible=" + mExpandedVisible + "flags=" + flags); } - - // TODO(b/62444020): remove when this bug is fixed - Log.v(TAG, "NotificationShadeWindow: " + getNotificationShadeWindowView() - + " canPanelBeCollapsed(): " - + getNotificationPanelViewController().canPanelBeCollapsed()); if (getNotificationShadeWindowView() != null - && getNotificationPanelViewController().canPanelBeCollapsed() + && mNotificationPanelViewController.canPanelBeCollapsed() && (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) { // release focus immediately to kick off focus change transition mNotificationShadeWindowController.setNotificationShadeFocusable(false); - getCentralSurfaces().getNotificationShadeWindowViewController().cancelExpandHelper(); - getNotificationPanelViewController() - .collapsePanel(true /* animate */, delayed, speedUpFactor); + mNotificationShadeWindowViewController.cancelExpandHelper(); + mNotificationPanelViewController.collapsePanel(true, delayed, speedUpFactor); } } - @Override public boolean closeShadeIfOpen() { - if (!getNotificationPanelViewController().isFullyCollapsed()) { + if (!mNotificationPanelViewController.isFullyCollapsed()) { mCommandQueue.animateCollapsePanels( CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); - getCentralSurfaces().visibilityChanged(false); + notifyVisibilityChanged(false); mAssistManagerLazy.get().hideAssist(); } return false; @@ -146,21 +155,19 @@ public class ShadeControllerImpl implements ShadeController { @Override public boolean isShadeOpen() { - NotificationPanelViewController controller = - getNotificationPanelViewController(); - return controller.isExpanding() || controller.isFullyExpanded(); + return mNotificationPanelViewController.isExpanding() + || mNotificationPanelViewController.isFullyExpanded(); } @Override public void postOnShadeExpanded(Runnable executable) { - getNotificationPanelViewController().addOnGlobalLayoutListener( + mNotificationPanelViewController.addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - if (getCentralSurfaces().getNotificationShadeWindowView() - .isVisibleToUser()) { - getNotificationPanelViewController().removeOnGlobalLayoutListener(this); - getNotificationPanelViewController().postToView(executable); + if (getNotificationShadeWindowView().isVisibleToUser()) { + mNotificationPanelViewController.removeOnGlobalLayoutListener(this); + mNotificationPanelViewController.postToView(executable); } } }); @@ -183,12 +190,11 @@ public class ShadeControllerImpl implements ShadeController { } @Override - public boolean collapsePanel() { - if (!getNotificationPanelViewController().isFullyCollapsed()) { + public boolean collapseShade() { + if (!mNotificationPanelViewController.isFullyCollapsed()) { // close the shade if it was open - animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, - true /* force */, true /* delayed */); - getCentralSurfaces().visibilityChanged(false); + animateCollapseShadeDelayed(); + notifyVisibilityChanged(false); return true; } else { @@ -197,33 +203,154 @@ public class ShadeControllerImpl implements ShadeController { } @Override - public void collapsePanel(boolean animate) { + public void collapseShade(boolean animate) { if (animate) { - boolean willCollapse = collapsePanel(); + boolean willCollapse = collapseShade(); if (!willCollapse) { runPostCollapseRunnables(); } - } else if (!getPresenter().isPresenterFullyCollapsed()) { - getCentralSurfaces().instantCollapseNotificationPanel(); - getCentralSurfaces().visibilityChanged(false); + } else if (!mPresenter.isPresenterFullyCollapsed()) { + instantCollapseShade(); + notifyVisibilityChanged(false); } else { runPostCollapseRunnables(); } } - private CentralSurfaces getCentralSurfaces() { - return mCentralSurfacesOptionalLazy.get().get(); + @Override + public void onStatusBarTouch(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (mExpandedVisible) { + animateCollapseShade(); + } + } } - private NotificationPresenter getPresenter() { - return getCentralSurfaces().getPresenter(); + @Override + public void onClosingFinished() { + runPostCollapseRunnables(); + if (!mPresenter.isPresenterFullyCollapsed()) { + // if we set it not to be focusable when collapsing, we have to undo it when we aborted + // the closing + mNotificationShadeWindowController.setNotificationShadeFocusable(true); + } } - protected NotificationShadeWindowView getNotificationShadeWindowView() { - return getCentralSurfaces().getNotificationShadeWindowView(); + @Override + public void instantCollapseShade() { + mNotificationPanelViewController.instantCollapse(); + runPostCollapseRunnables(); + } + + @Override + public void makeExpandedVisible(boolean force) { + if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible); + if (!force && (mExpandedVisible || !mCommandQueue.panelsEnabled())) { + return; + } + + mExpandedVisible = true; + + // Expand the window to encompass the full screen in anticipation of the drag. + // It's only possible to do atomically because the status bar is at the top of the screen! + mNotificationShadeWindowController.setPanelVisible(true); + + notifyVisibilityChanged(true); + mCommandQueue.recomputeDisableFlags(mDisplayId, !force /* animate */); + notifyExpandedVisibleChanged(true); } - private NotificationPanelViewController getNotificationPanelViewController() { - return getCentralSurfaces().getNotificationPanelViewController(); + @Override + public void makeExpandedInvisible() { + if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible); + + if (!mExpandedVisible || getNotificationShadeWindowView() == null) { + return; + } + + // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868) + mNotificationPanelViewController.collapsePanel(false, false, 1.0f); + + mNotificationPanelViewController.closeQs(); + + mExpandedVisible = false; + notifyVisibilityChanged(false); + + // Update the visibility of notification shade and status bar window. + mNotificationShadeWindowController.setPanelVisible(false); + mStatusBarWindowController.setForceStatusBarVisible(false); + + // Close any guts that might be visible + mGutsManager.get().closeAndSaveGuts( + true /* removeLeavebehind */, + true /* force */, + true /* removeControls */, + -1 /* x */, + -1 /* y */, + true /* resetMenu */); + + runPostCollapseRunnables(); + notifyExpandedVisibleChanged(false); + mCommandQueue.recomputeDisableFlags( + mDisplayId, + mNotificationPanelViewController.hideStatusBarIconsWhenExpanded() /* animate */); + + // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in + // the bouncer appear animation. + if (!mKeyguardStateController.isShowing()) { + WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); + } + } + + @Override + public boolean isExpandedVisible() { + return mExpandedVisible; + } + + @Override + public void setVisibilityListener(ShadeVisibilityListener listener) { + mShadeVisibilityListener = listener; + } + + private void notifyVisibilityChanged(boolean visible) { + mShadeVisibilityListener.visibilityChanged(visible); + } + + private void notifyExpandedVisibleChanged(boolean expandedVisible) { + mShadeVisibilityListener.expandedVisibleChanged(expandedVisible); + } + + @Override + public void setNotificationPresenter(NotificationPresenter presenter) { + mPresenter = presenter; + } + + @Override + public void setNotificationShadeWindowViewController( + NotificationShadeWindowViewController controller) { + mNotificationShadeWindowViewController = controller; + } + + private NotificationShadeWindowView getNotificationShadeWindowView() { + return mNotificationShadeWindowViewController.getView(); + } + + @Override + public void setNotificationPanelViewController( + NotificationPanelViewController notificationPanelViewController) { + mNotificationPanelViewController = notificationPanelViewController; + mNotificationPanelViewController.setTrackingStartedListener(this::runPostCollapseRunnables); + mNotificationPanelViewController.setOpenCloseListener( + new NotificationPanelViewController.OpenCloseListener() { + @Override + public void onClosingFinished() { + ShadeControllerImpl.this.onClosingFinished(); + } + + @Override + public void onOpenStarted() { + makeExpandedVisible(false); + } + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index bc456d5d4613..2334a4c27af8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -15,6 +15,7 @@ import android.os.Trace import android.util.AttributeSet import android.util.MathUtils.lerp import android.view.View +import android.view.animation.PathInterpolator import com.android.systemui.animation.Interpolators import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold import com.android.systemui.util.getColorWithAlpha @@ -88,10 +89,12 @@ object LiftReveal : LightRevealEffect { class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect { - private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE + // Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster + private val interpolator = PathInterpolator(/* controlX1= */ 0.4f, /* controlY1= */ 0f, + /* controlX2= */ 0.2f, /* controlY2= */ 1f) override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { - val interpolatedAmount = INTERPOLATOR.getInterpolation(amount) + val interpolatedAmount = interpolator.getInterpolation(amount) scrim.interpolatedRevealAmount = interpolatedAmount diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index cdefae6b87f9..f4cd985adbdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -30,6 +30,7 @@ import android.content.IntentSender; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -37,6 +38,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.statusbar.NotificationVisibility; @@ -127,21 +129,6 @@ public class NotificationLockscreenUserManagerImpl implements public void onReceive(Context context, Intent intent) { String action = intent.getAction(); switch (action) { - case Intent.ACTION_USER_SWITCHED: - mCurrentUserId = intent.getIntExtra( - Intent.EXTRA_USER_HANDLE, UserHandle.USER_ALL); - updateCurrentProfilesCache(); - - Log.v(TAG, "userId " + mCurrentUserId + " is in the house"); - - updateLockscreenNotificationSetting(); - updatePublicMode(); - mPresenter.onUserSwitched(mCurrentUserId); - - for (UserChangedListener listener : mListeners) { - listener.onUserChanged(mCurrentUserId); - } - break; case Intent.ACTION_USER_REMOVED: int removedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (removedUserId != -1) { @@ -181,6 +168,25 @@ public class NotificationLockscreenUserManagerImpl implements } }; + protected final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mCurrentUserId = newUser; + updateCurrentProfilesCache(); + + Log.v(TAG, "userId " + mCurrentUserId + " is in the house"); + + updateLockscreenNotificationSetting(); + updatePublicMode(); + mPresenter.onUserSwitched(mCurrentUserId); + + for (UserChangedListener listener : mListeners) { + listener.onUserChanged(mCurrentUserId); + } + } + }; + protected final Context mContext; private final Handler mMainHandler; protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>(); @@ -284,7 +290,6 @@ public class NotificationLockscreenUserManagerImpl implements null /* handler */, UserHandle.ALL); IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_UNLOCKED); @@ -298,6 +303,8 @@ public class NotificationLockscreenUserManagerImpl implements mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null, Context.RECEIVER_EXPORTED_UNAUDITED); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler)); + mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late updateCurrentProfilesCache(); 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 7eb890677206..39daa13ae168 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -44,4 +44,8 @@ class NotifPipelineFlags @Inject constructor( val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy { featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD) } + + val isNoHunForOldWhenEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.NO_HUN_FOR_OLD_WHEN) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index d97b712df030..3e2dd053d938 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -38,6 +38,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min + @SysUISingleton class NotificationWakeUpCoordinator @Inject constructor( dumpManager: DumpManager, @@ -45,7 +46,8 @@ class NotificationWakeUpCoordinator @Inject constructor( private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, private val dozeParameters: DozeParameters, - private val screenOffAnimationController: ScreenOffAnimationController + private val screenOffAnimationController: ScreenOffAnimationController, + private val logger: NotificationWakeUpCoordinatorLogger, ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener, Dumpable { @@ -242,6 +244,7 @@ class NotificationWakeUpCoordinator @Inject constructor( } override fun onDozeAmountChanged(linear: Float, eased: Float) { + logger.logOnDozeAmountChanged(linear, eased) if (overrideDozeAmountIfAnimatingScreenOff(linear)) { return } @@ -273,6 +276,7 @@ class NotificationWakeUpCoordinator @Inject constructor( } override fun onStateChanged(newState: Int) { + logger.logOnStateChanged(newState) if (state == StatusBarState.SHADE && newState == StatusBarState.SHADE) { // The SHADE -> SHADE transition is only possible as part of cancelling the screen-off // animation (e.g. by fingerprint unlock). This is done because the system is in an @@ -320,8 +324,12 @@ class NotificationWakeUpCoordinator @Inject constructor( private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { if (statusBarStateController.state == StatusBarState.KEYGUARD) { + logger.logSetDozeAmount("1.0", "1.0", + "Override: bypass (keyguard)", StatusBarState.KEYGUARD) setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)") } else { + logger.logSetDozeAmount("0.0", "0.0", + "Override: bypass (shade)", statusBarStateController.state) setDozeAmount(0f, 0f, source = "Override: bypass (shade)") } return true diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt new file mode 100644 index 000000000000..b40ce25c58d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt @@ -0,0 +1,55 @@ +/* + * 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 + +import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import javax.inject.Inject + +class NotificationWakeUpCoordinatorLogger +@Inject +constructor(@NotificationLog private val buffer: LogBuffer) { + fun logSetDozeAmount(linear: String, eased: String, source: String, state: Int) { + buffer.log( + TAG, + DEBUG, + { + str1 = linear + str2 = eased + str3 = source + int1 = state + }, + { "setDozeAmount: linear: $str1, eased: $str2, source: $str3, state: $int1" } + ) + } + + fun logOnDozeAmountChanged(linear: Float, eased: Float) { + buffer.log( + TAG, + DEBUG, + { + double1 = linear.toDouble() + str2 = eased.toString() + }, + { "onDozeAmountChanged($double1, $str2)" } + ) + } + + fun logOnStateChanged(newState: Int) { + buffer.log(TAG, DEBUG, { int1 = newState }, { "onStateChanged($int1)" }) + } +} + +private const val TAG = "NotificationWakeUpCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index e6dbcee10f60..7513aa7fa2a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -2,22 +2,20 @@ package com.android.systemui.statusbar.notification.interruption import android.app.Notification import android.app.Notification.VISIBILITY_SECRET -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.database.ContentObserver import android.net.Uri import android.os.Handler +import android.os.HandlerExecutor import android.os.UserHandle import android.provider.Settings import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.CoreStartable -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -78,7 +76,7 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val highPriorityProvider: HighPriorityProvider, private val statusBarStateController: SysuiStatusBarStateController, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val secureSettings: SecureSettings, private val globalSettings: GlobalSettings ) : CoreStartable, KeyguardNotificationVisibilityProvider { @@ -87,6 +85,15 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( private val onStateChangedListeners = ListenerSet<Consumer<String>>() private var hideSilentNotificationsOnLockscreen: Boolean = false + private val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + if (isLockedOrLocking) { + // maybe public mode changed + notifyStateChanged("onUserSwitched") + } + } + } + override fun start() { readShowSilentNotificationSetting() keyguardStateController.addCallback(object : KeyguardStateController.Callback { @@ -143,14 +150,7 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( notifyStateChanged("onStatusBarUpcomingStateChanged") } }) - broadcastDispatcher.registerReceiver(object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (isLockedOrLocking) { - // maybe public mode changed - notifyStateChanged(intent.action!!) - } - } - }, IntentFilter(Intent.ACTION_USER_SWITCHED)) + userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler)) } override fun addOnStateChangedListener(listener: Consumer<String>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 073b6b041b81..13b3aca2a88f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -106,6 +106,36 @@ class NotificationInterruptLogger @Inject constructor( }) } + fun logNoHeadsUpOldWhen( + entry: NotificationEntry, + notifWhen: Long, + notifAge: Long + ) { + buffer.log(TAG, DEBUG, { + str1 = entry.logKey + long1 = notifWhen + long2 = notifAge + }, { + "No heads up: old when $long1 (age=$long2 ms): $str1" + }) + } + + fun logMaybeHeadsUpDespiteOldWhen( + entry: NotificationEntry, + notifWhen: Long, + notifAge: Long, + reason: String + ) { + buffer.log(TAG, DEBUG, { + str1 = entry.logKey + str2 = reason + long1 = notifWhen + long2 = notifAge + }, { + "Maybe heads up: old when $long1 (age=$long2 ms) but $str2: $str1" + }) + } + fun logNoHeadsUpSuppressedBy( entry: NotificationEntry, suppressor: NotificationInterruptSuppressor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index c4f5a3a30608..ec5bd6894283 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR; +import android.app.Notification; import android.app.NotificationManager; import android.content.ContentResolver; import android.database.ContentObserver; @@ -82,7 +83,10 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235), @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard") - FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236); + FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236), + + @UiEvent(doc = "HUN suppressed for old when") + HUN_SUPPRESSED_OLD_WHEN(1237); private final int mId; @@ -346,6 +350,10 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter return false; } + if (shouldSuppressHeadsUpWhenAwakeForOldWhen(entry, log)) { + return false; + } + for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) { if (log) mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i)); @@ -470,4 +478,51 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private boolean isSnoozedPackage(StatusBarNotification sbn) { return mHeadsUpManager.isSnoozed(sbn.getPackageName()); } + + private boolean shouldSuppressHeadsUpWhenAwakeForOldWhen(NotificationEntry entry, boolean log) { + if (!mFlags.isNoHunForOldWhenEnabled()) { + return false; + } + + final Notification notification = entry.getSbn().getNotification(); + if (notification == null) { + return false; + } + + final long when = notification.when; + final long now = System.currentTimeMillis(); + final long age = now - when; + + if (age < MAX_HUN_WHEN_AGE_MS) { + return false; + } + + if (when <= 0) { + // Some notifications (including many system notifications) are posted with the "when" + // field set to 0. Nothing in the Javadocs for Notification mentions a special meaning + // for a "when" of 0, but Android didn't even exist at the dawn of the Unix epoch. + // Therefore, assume that these notifications effectively don't have a "when" value, + // and don't suppress HUNs. + if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "when <= 0"); + return false; + } + + if (notification.fullScreenIntent != null) { + if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "full-screen intent"); + return false; + } + + if (notification.isForegroundService()) { + if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "foreground service"); + return false; + } + + if (log) mLogger.logNoHeadsUpOldWhen(entry, when, age); + final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); + mUiEventLogger.log(NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN, uid, packageName); + return true; + } + + public static final long MAX_HUN_WHEN_AGE_MS = 24 * 60 * 60 * 1000; } 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 1eccc9879a87..d7d5ac961249 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 @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY; import static android.service.notification.NotificationListenerService.REASON_CANCEL; +import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; import android.animation.Animator; @@ -1404,6 +1405,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mKeepInParentForDismissAnimation = keepInParent; } + /** @return true if the User has dismissed this notif's parent */ + public boolean isParentDismissed() { + return getEntry().getDismissState() == PARENT_DISMISSED; + } + @Override public boolean isRemoved() { return mRemoved; 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 f9e9a2d38138..8a400d5fef99 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 @@ -359,10 +359,15 @@ public class ExpandableNotificationRowController implements NotifViewController @Override public boolean offerToKeepInParentForAnimation() { - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_DISMISSAL_ANIMATION)) { + //If the User dismissed the notification's parent, we want to keep it attached until the + //dismiss animation is ongoing. Therefore we don't want to remove it in the ShadeViewDiffer. + if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_DISMISSAL_ANIMATION) + && mView.isParentDismissed()) { mView.setKeepInParentForDismissAnimation(true); return true; } + + //Otherwise the view system doesn't do the removal, so we rely on the ShadeViewDiffer return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 64f87cabaf74..b56bae12be6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -54,8 +54,6 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.policy.HeadsUpManager; -import java.util.Collections; - import javax.inject.Inject; /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 0ce9656a21b5..f21db0bde59a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -154,7 +154,7 @@ public class NotificationConversationInfo extends LinearLayout implements // If the user selected Priority and the previous selection was not priority, show a // People Tile add request. if (mSelectedAction == ACTION_FAVORITE && getPriority() != mSelectedAction) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); mPeopleSpaceWidgetManager.requestPinAppWidget(mShortcutInfo, new Bundle()); } mGutsContainer.closeControls(v, /* save= */ true); 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 073bd4bf302b..b519aefcd4c9 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 @@ -1401,10 +1401,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mExpandedHeight = height; setIsExpanded(height > 0); int minExpansionHeight = getMinExpansionHeight(); - if (height < minExpansionHeight) { + if (height < minExpansionHeight && !mShouldUseSplitNotificationShade) { mClipRect.left = 0; mClipRect.right = getWidth(); - mClipRect.top = getNotificationsClippingTopBound(); + mClipRect.top = 0; mClipRect.bottom = (int) height; height = minExpansionHeight; setRequestedClipBounds(mClipRect); @@ -1466,17 +1466,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable notifyAppearChangedListeners(); } - private int getNotificationsClippingTopBound() { - if (isHeadsUpTransition()) { - // HUN in split shade can go higher than bottom of NSSL when swiping up so we want - // to give it extra clipping margin. Because clipping has rounded corners, we also - // need to account for that corner clipping. - return -mAmbientState.getStackTopMargin() - mCornerRadius; - } else { - return 0; - } - } - private void notifyAppearChangedListeners() { float appear; float expandAmount; @@ -4236,7 +4225,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mShadeNeedsToClose = false; postDelayed( () -> { - mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); + mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); }, DELAY_BEFORE_SHADE_CLOSE /* delayMillis */); } @@ -5139,6 +5128,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable println(pw, "intrinsicPadding", mIntrinsicPadding); println(pw, "topPadding", mTopPadding); println(pw, "bottomPadding", mBottomPadding); + mNotificationStackSizeCalculator.dump(pw, args); }); pw.println(); pw.println("Contents:"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index ae854e2df91a..25f99c69d454 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.util.Compile import com.android.systemui.util.children +import java.io.PrintWriter import javax.inject.Inject import kotlin.math.max import kotlin.math.min @@ -53,6 +54,8 @@ constructor( @Main private val resources: Resources ) { + private lateinit var lastComputeHeightLog : String + /** * Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow shelf. * If there are exactly 1 + mMaxKeyguardNotifications, and they fit in the available space @@ -114,7 +117,9 @@ constructor( shelfIntrinsicHeight: Float ): Int { log { "\n" } - val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight) + + val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight, + /* computeHeight= */ false) var maxNotifications = stackHeightSequence.lastIndexWhile { heightResult -> @@ -157,18 +162,21 @@ constructor( shelfIntrinsicHeight: Float ): Float { log { "\n" } + lastComputeHeightLog = "" val heightPerMaxNotifications = - computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight) + computeHeightPerNotificationLimit(stack, shelfIntrinsicHeight, + /* computeHeight= */ true) val (notificationsHeight, shelfHeightWithSpaceBefore) = heightPerMaxNotifications.elementAtOrElse(maxNotifications) { heightPerMaxNotifications.last() // Height with all notifications visible. } - log { - "computeHeight(maxNotifications=$maxNotifications," + + lastComputeHeightLog += "\ncomputeHeight(maxNotifications=$maxNotifications," + "shelfIntrinsicHeight=$shelfIntrinsicHeight) -> " + "${notificationsHeight + shelfHeightWithSpaceBefore}" + " = ($notificationsHeight + $shelfHeightWithSpaceBefore)" + log { + lastComputeHeightLog } return notificationsHeight + shelfHeightWithSpaceBefore } @@ -184,7 +192,8 @@ constructor( private fun computeHeightPerNotificationLimit( stack: NotificationStackScrollLayout, - shelfHeight: Float + shelfHeight: Float, + computeHeight: Boolean ): Sequence<StackHeight> = sequence { log { "computeHeightPerNotificationLimit" } @@ -213,9 +222,14 @@ constructor( currentIndex = firstViewInShelfIndex) spaceBeforeShelf + shelfHeight } + + val currentLog = "computeHeight | i=$i notificationsHeight=$notifications " + + "shelfHeightWithSpaceBefore=$shelfWithSpaceBefore" + if (computeHeight) { + lastComputeHeightLog += "\n" + currentLog + } log { - "i=$i notificationsHeight=$notifications " + - "shelfHeightWithSpaceBefore=$shelfWithSpaceBefore" + currentLog } yield( StackHeight( @@ -260,6 +274,10 @@ constructor( return size } + fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("NotificationStackSizeCalculator lastComputeHeightLog = $lastComputeHeightLog") + } + private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean { if (visibility == GONE || hasNoContentHeight()) return false if (onLockscreen) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 3557b4ab34f5..c6f64f3e56ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -51,7 +51,6 @@ import com.android.systemui.qs.QSPanelController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; -import com.android.systemui.statusbar.GestureRecorder; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.NotificationPresenter; @@ -191,8 +190,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void animateExpandSettingsPanel(@Nullable String subpanel); - void animateCollapsePanels(int flags, boolean force); - void collapsePanelOnMainThread(); void togglePanel(); @@ -280,8 +277,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void postAnimateOpenPanels(); - boolean isExpandedVisible(); - boolean isPanelExpanded(); void onInputFocusTransfer(boolean start, boolean cancel, float velocity); @@ -290,8 +285,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void onTouchEvent(MotionEvent event); - GestureRecorder getGestureRecorder(); - BiometricUnlockController getBiometricUnlockController(); void showWirelessChargingAnimation(int batteryLevel); @@ -406,10 +399,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn LightRevealScrim getLightRevealScrim(); - void onTrackingStarted(); - - void onClosingFinished(); - // TODO: Figure out way to remove these. NavigationBarView getNavigationBarView(); @@ -493,12 +482,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void updateNotificationPanelTouchState(); - void makeExpandedVisible(boolean force); - - void instantCollapseNotificationPanel(); - - void visibilityChanged(boolean visible); - int getDisplayId(); int getRotation(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 9e5a66f1e306..72ada0e17a01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -209,7 +209,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba public void animateExpandNotificationsPanel() { if (CentralSurfaces.SPEW) { Log.d(CentralSurfaces.TAG, - "animateExpand: mExpandedVisible=" + mCentralSurfaces.isExpandedVisible()); + "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible()); } if (!mCommandQueue.panelsEnabled()) { return; @@ -222,7 +222,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba public void animateExpandSettingsPanel(@Nullable String subPanel) { if (CentralSurfaces.SPEW) { Log.d(CentralSurfaces.TAG, - "animateExpand: mExpandedVisible=" + mCentralSurfaces.isExpandedVisible()); + "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible()); } if (!mCommandQueue.panelsEnabled()) { return; @@ -276,7 +276,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) { if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); } } @@ -293,7 +293,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if ((diff2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) { mCentralSurfaces.updateQsExpansionEnabled(); if ((state2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); } } @@ -550,7 +550,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba @Override public void togglePanel() { if (mCentralSurfaces.isPanelExpanded()) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); } else { animateExpandNotificationsPanel(); } 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 783d183f0ddf..00a9916f5ac1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -58,7 +58,6 @@ import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; -import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -423,12 +422,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { /** */ @Override - public void animateCollapsePanels(int flags, boolean force) { - mCommandQueueCallbacks.animateCollapsePanels(flags, force); - } - - /** */ - @Override public void togglePanel() { mCommandQueueCallbacks.togglePanel(); } @@ -510,8 +503,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private View mReportRejectedTouch; - private boolean mExpandedVisible; - private final NotificationGutsManager mGutsManager; private final NotificationLogger mNotificationLogger; private final ShadeExpansionStateManager mShadeExpansionStateManager; @@ -910,6 +901,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { updateDisplaySize(); mStatusBarHideIconsForBouncerManager.setDisplayId(mDisplayId); + initShadeVisibilityListener(); + // start old BaseStatusBar.start(). mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService( @@ -1100,6 +1093,25 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { requestTopUi, componentTag)))); } + @VisibleForTesting + void initShadeVisibilityListener() { + mShadeController.setVisibilityListener(new ShadeController.ShadeVisibilityListener() { + @Override + public void visibilityChanged(boolean visible) { + onShadeVisibilityChanged(visible); + } + + @Override + public void expandedVisibleChanged(boolean expandedVisible) { + if (expandedVisible) { + setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true); + } else { + onExpandedInvisible(); + } + } + }); + } + private void onFoldedStateChanged(boolean isFolded, boolean willGoToSleep) { Trace.beginSection("CentralSurfaces#onFoldedStateChanged"); onFoldedStateChangedInternal(isFolded, willGoToSleep); @@ -1245,7 +1257,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationPanelViewController.initDependencies( this, - this::makeExpandedInvisible, + mGestureRec, + mShadeController::makeExpandedInvisible, mNotificationShelfController); BackDropView backdrop = mNotificationShadeWindowView.findViewById(R.id.backdrop); @@ -1448,6 +1461,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mRemoteInputManager.addControllerCallback(mNotificationShadeWindowController); mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter); mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter); + mShadeController.setNotificationPresenter(mPresenter); mNotificationsController.initialize( this, mPresenter, @@ -1497,11 +1511,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (v, event) -> { mAutoHideController.checkUserAutoHide(event); mRemoteInputManager.checkRemoteInputOutside(event); - if (event.getAction() == MotionEvent.ACTION_UP) { - if (mExpandedVisible) { - mShadeController.animateCollapsePanels(); - } - } + mShadeController.onStatusBarTouch(event); return mNotificationShadeWindowView.onTouchEvent(event); }; } @@ -1523,6 +1533,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowViewController.setupExpandedStatusBar(); mNotificationPanelViewController = mCentralSurfacesComponent.getNotificationPanelViewController(); + mShadeController.setNotificationPanelViewController(mNotificationPanelViewController); + mShadeController.setNotificationShadeWindowViewController( + mNotificationShadeWindowViewController); mCentralSurfacesComponent.getLockIconViewController().init(); mStackScrollerController = mCentralSurfacesComponent.getNotificationStackScrollLayoutController(); @@ -1843,9 +1856,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { public void onLaunchAnimationCancelled(boolean isLaunchForActivity) { if (mPresenter.isPresenterFullyCollapsed() && !mPresenter.isCollapsing() && isLaunchForActivity) { - onClosingFinished(); + mShadeController.onClosingFinished(); } else { - mShadeController.collapsePanel(true /* animate */); + mShadeController.collapseShade(true /* animate */); } } @@ -1853,10 +1866,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onLaunchAnimationEnd(boolean launchIsFullScreen) { if (!mPresenter.isCollapsing()) { - onClosingFinished(); + mShadeController.onClosingFinished(); } if (launchIsFullScreen) { - instantCollapseNotificationPanel(); + mShadeController.instantCollapseShade(); } } @@ -1948,33 +1961,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public void makeExpandedVisible(boolean force) { - if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible); - if (!force && (mExpandedVisible || !mCommandQueue.panelsEnabled())) { - return; - } - - mExpandedVisible = true; - - // Expand the window to encompass the full screen in anticipation of the drag. - // This is only possible to do atomically because the status bar is at the top of the screen! - mNotificationShadeWindowController.setPanelVisible(true); - - visibilityChanged(true); - mCommandQueue.recomputeDisableFlags(mDisplayId, !force /* animate */); - setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true); - } - - @Override public void postAnimateCollapsePanels() { - mMainExecutor.execute(mShadeController::animateCollapsePanels); + mMainExecutor.execute(mShadeController::animateCollapseShade); } @Override public void postAnimateForceCollapsePanels() { - mMainExecutor.execute( - () -> mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, - true /* force */)); + mMainExecutor.execute(mShadeController::animateCollapseShadeForced); } @Override @@ -1983,11 +1976,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public boolean isExpandedVisible() { - return mExpandedVisible; - } - - @Override public boolean isPanelExpanded() { return mPanelExpanded; } @@ -2016,46 +2004,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } - void makeExpandedInvisible() { - if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible); - - if (!mExpandedVisible || mNotificationShadeWindowView == null) { - return; - } - - // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868) - mNotificationPanelViewController.collapsePanel(/*animate=*/ false, false /* delayed*/, - 1.0f /* speedUpFactor */); - - mNotificationPanelViewController.closeQs(); - - mExpandedVisible = false; - visibilityChanged(false); - - // Update the visibility of notification shade and status bar window. - mNotificationShadeWindowController.setPanelVisible(false); - mStatusBarWindowController.setForceStatusBarVisible(false); - - // Close any guts that might be visible - mGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, - true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */); - - mShadeController.runPostCollapseRunnables(); + private void onExpandedInvisible() { setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false); if (!mNotificationActivityStarter.isCollapsingToShowActivityOverLockscreen()) { showBouncerOrLockScreenIfKeyguard(); } else if (DEBUG) { Log.d(TAG, "Not showing bouncer due to activity showing over lockscreen"); } - mCommandQueue.recomputeDisableFlags( - mDisplayId, - mNotificationPanelViewController.hideStatusBarIconsWhenExpanded() /* animate */); - - // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in - // the bouncer appear animation. - if (!mKeyguardStateController.isShowing()) { - WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); - } } /** Called when a touch event occurred on {@link PhoneStatusBarView}. */ @@ -2092,16 +2047,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { final boolean upOrCancel = event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL; - setInteracting(StatusBarManager.WINDOW_STATUS_BAR, !upOrCancel || mExpandedVisible); + setInteracting(StatusBarManager.WINDOW_STATUS_BAR, + !upOrCancel || mShadeController.isExpandedVisible()); } } @Override - public GestureRecorder getGestureRecorder() { - return mGestureRec; - } - - @Override public BiometricUnlockController getBiometricUnlockController() { return mBiometricUnlockController; } @@ -2241,7 +2192,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); synchronized (mQueueLock) { pw.println("Current Status Bar state:"); - pw.println(" mExpandedVisible=" + mExpandedVisible); + pw.println(" mExpandedVisible=" + mShadeController.isExpandedVisible()); pw.println(" mDisplayMetrics=" + mDisplayMetrics); pw.println(" mStackScroller: " + CentralSurfaces.viewInfo(mStackScroller)); pw.println(" mStackScroller: " + CentralSurfaces.viewInfo(mStackScroller) @@ -2556,10 +2507,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } if (dismissShade) { - if (mExpandedVisible && !mBouncerShowing) { - mShadeController.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, - true /* force */, true /* delayed*/); + if (mShadeController.isExpandedVisible() && !mBouncerShowing) { + mShadeController.animateCollapseShadeDelayed(); } else { // Do it after DismissAction has been processed to conserve the needed // ordering. @@ -2601,7 +2550,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { flags |= CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL; } } - mShadeController.animateCollapsePanels(flags); + mShadeController.animateCollapseShade(flags); } } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { if (mNotificationShadeWindowController != null) { @@ -2716,10 +2665,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { com.android.systemui.R.dimen.physical_power_button_center_screen_location_y)); } - // Visibility reporting protected void handleVisibleToUserChanged(boolean visibleToUser) { if (visibleToUser) { - handleVisibleToUserChangedImpl(visibleToUser); + onVisibleToUser(); mNotificationLogger.startNotificationLogging(); if (!mIsBackCallbackRegistered) { @@ -2736,7 +2684,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } else { mNotificationLogger.stopNotificationLogging(); - handleVisibleToUserChangedImpl(visibleToUser); + onInvisibleToUser(); if (mIsBackCallbackRegistered) { ViewRootImpl viewRootImpl = getViewRootImpl(); @@ -2756,41 +2704,38 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } - // Visibility reporting - void handleVisibleToUserChangedImpl(boolean visibleToUser) { - if (visibleToUser) { - /* The LEDs are turned off when the notification panel is shown, even just a little bit. - * See also CentralSurfaces.setPanelExpanded for another place where we attempt to do - * this. - */ - boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp(); - boolean clearNotificationEffects = - !mPresenter.isPresenterFullyCollapsed() && - (mState == StatusBarState.SHADE - || mState == StatusBarState.SHADE_LOCKED); - int notificationLoad = mNotificationsController.getActiveNotificationsCount(); - if (pinnedHeadsUp && mPresenter.isPresenterFullyCollapsed()) { - notificationLoad = 1; - } - final int finalNotificationLoad = notificationLoad; - mUiBgExecutor.execute(() -> { - try { - mBarService.onPanelRevealed(clearNotificationEffects, - finalNotificationLoad); - } catch (RemoteException ex) { - // Won't fail unless the world has ended. - } - }); - } else { - mUiBgExecutor.execute(() -> { - try { - mBarService.onPanelHidden(); - } catch (RemoteException ex) { - // Won't fail unless the world has ended. - } - }); + void onVisibleToUser() { + /* The LEDs are turned off when the notification panel is shown, even just a little bit. + * See also CentralSurfaces.setPanelExpanded for another place where we attempt to do + * this. + */ + boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp(); + boolean clearNotificationEffects = + !mPresenter.isPresenterFullyCollapsed() && (mState == StatusBarState.SHADE + || mState == StatusBarState.SHADE_LOCKED); + int notificationLoad = mNotificationsController.getActiveNotificationsCount(); + if (pinnedHeadsUp && mPresenter.isPresenterFullyCollapsed()) { + notificationLoad = 1; } + final int finalNotificationLoad = notificationLoad; + mUiBgExecutor.execute(() -> { + try { + mBarService.onPanelRevealed(clearNotificationEffects, + finalNotificationLoad); + } catch (RemoteException ex) { + // Won't fail unless the world has ended. + } + }); + } + void onInvisibleToUser() { + mUiBgExecutor.execute(() -> { + try { + mBarService.onPanelHidden(); + } catch (RemoteException ex) { + // Won't fail unless the world has ended. + } + }); } private void logStateToEventlog() { @@ -2968,7 +2913,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void updatePanelExpansionForKeyguard() { if (mState == StatusBarState.KEYGUARD && mBiometricUnlockController.getMode() != BiometricUnlockController.MODE_WAKE_AND_UNLOCK && !mBouncerShowing) { - mShadeController.instantExpandNotificationsPanel(); + mShadeController.instantExpandShade(); } } @@ -3087,7 +3032,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // too heavy for the CPU and GPU on any device. mNavigationBarController.disableAnimationsDuringHide(mDisplayId, delay); } else if (!mNotificationPanelViewController.isCollapsing()) { - instantCollapseNotificationPanel(); + mShadeController.instantCollapseShade(); } // Keyguard state has changed, but QS is not listening anymore. Make sure to update the tile @@ -3245,8 +3190,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public boolean onMenuPressed() { if (shouldUnlockOnMenuPressed()) { - mShadeController.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */); + mShadeController.animateCollapseShadeForced(); return true; } return false; @@ -3291,7 +3235,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED && !isBouncerShowingOverDream()) { if (mNotificationPanelViewController.canPanelBeCollapsed()) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); } return true; } @@ -3301,8 +3245,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public boolean onSpacePressed() { if (mDeviceInteractive && mState != StatusBarState.SHADE) { - mShadeController.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL /* flags */, true /* force */); + mShadeController.animateCollapseShadeForced(); return true; } return false; @@ -3341,12 +3284,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } - @Override - public void instantCollapseNotificationPanel() { - mNotificationPanelViewController.instantCollapse(); - mShadeController.runPostCollapseRunnables(); - } - /** * Collapse the panel directly if we are on the main thread, post the collapsing on the main * thread if we are not. @@ -3354,9 +3291,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void collapsePanelOnMainThread() { if (Looper.getMainLooper().isCurrentThread()) { - mShadeController.collapsePanel(); + mShadeController.collapseShade(); } else { - mContext.getMainExecutor().execute(mShadeController::collapsePanel); + mContext.getMainExecutor().execute(mShadeController::collapseShade); } } @@ -3397,21 +3334,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mLightRevealScrim; } - @Override - public void onTrackingStarted() { - mShadeController.runPostCollapseRunnables(); - } - - @Override - public void onClosingFinished() { - mShadeController.runPostCollapseRunnables(); - if (!mPresenter.isPresenterFullyCollapsed()) { - // if we set it not to be focusable when collapsing, we have to undo it when we aborted - // the closing - mNotificationShadeWindowController.setNotificationShadeFocusable(true); - } - } - // TODO: Figure out way to remove these. @Override public NavigationBarView getNavigationBarView() { @@ -3496,7 +3418,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowViewController.cancelCurrentTouch(); } if (mPanelExpanded && mState == StatusBarState.SHADE) { - mShadeController.animateCollapsePanels(); + mShadeController.animateCollapseShade(); } } @@ -3559,7 +3481,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // The unlocked screen off and fold to aod animations might use our LightRevealScrim - // we need to be expanded for it to be visible. if (mDozeParameters.shouldShowLightRevealScrim()) { - makeExpandedVisible(true); + mShadeController.makeExpandedVisible(true); } DejankUtils.stopDetectingBlockingIpcs(tag); @@ -3588,7 +3510,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // If we are waking up during the screen off animation, we should undo making the // expanded visible (we did that so the LightRevealScrim would be visible). if (mScreenOffAnimationController.shouldHideLightRevealScrimOnWakeUp()) { - makeExpandedInvisible(); + mShadeController.makeExpandedInvisible(); } }); @@ -3923,8 +3845,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0); if (BANNER_ACTION_SETUP.equals(action)) { - mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, - true /* force */); + mShadeController.animateCollapseShadeForced(); mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -3986,7 +3907,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { action.run(); }).start(); - return collapsePanel ? mShadeController.collapsePanel() : willAnimateOnKeyguard; + return collapsePanel ? mShadeController.collapseShade() : willAnimateOnKeyguard; } @Override @@ -4081,8 +4002,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mMainExecutor.execute(runnable); } - @Override - public void visibilityChanged(boolean visible) { + private void onShadeVisibilityChanged(boolean visible) { if (mVisible != visible) { mVisible = visible; if (!visible) { 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 aa0757e1d572..000fe140882c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -240,8 +240,8 @@ public class KeyguardBouncer { && !mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( KeyguardUpdateMonitor.getCurrentUser()) && !needsFullscreenBouncer() - && !mKeyguardUpdateMonitor.isFaceLockedOut() - && !mKeyguardUpdateMonitor.userNeedsStrongAuth() + && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE) && !mKeyguardBypassController.getBypassEnabled()) { mHandler.postDelayed(mShowRunnable, BOUNCER_FACE_DELAY); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index 26e6db664e07..4beb87ddae2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -15,23 +15,21 @@ package com.android.systemui.statusbar.phone; import android.app.StatusBarManager; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; import androidx.annotation.NonNull; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -43,9 +41,9 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { private final List<Callback> mCallbacks = new ArrayList<>(); private final Context mContext; + private final Executor mMainExecutor; private final UserManager mUserManager; private final UserTracker mUserTracker; - private final BroadcastDispatcher mBroadcastDispatcher; private final LinkedList<UserInfo> mProfiles; private boolean mListening; private int mCurrentUser; @@ -53,12 +51,12 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { /** */ @Inject - public ManagedProfileControllerImpl(Context context, UserTracker userTracker, - BroadcastDispatcher broadcastDispatcher) { + public ManagedProfileControllerImpl(Context context, @Main Executor mainExecutor, + UserTracker userTracker) { mContext = context; + mMainExecutor = mainExecutor; mUserManager = UserManager.get(mContext); mUserTracker = userTracker; - mBroadcastDispatcher = broadcastDispatcher; mProfiles = new LinkedList<UserInfo>(); } @@ -130,30 +128,34 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { } private void setListening(boolean listening) { + if (mListening == listening) { + return; + } mListening = listening; if (listening) { reloadManagedProfiles(); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_SWITCHED); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); - mBroadcastDispatcher.registerReceiver( - mReceiver, filter, null /* handler */, UserHandle.ALL); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } else { - mBroadcastDispatcher.unregisterReceiver(mReceiver); + mUserTracker.removeCallback(mUserChangedCallback); } } - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - reloadManagedProfiles(); - for (Callback callback : mCallbacks) { - callback.onManagedProfileChanged(); - } - } - }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + reloadManagedProfiles(); + for (Callback callback : mCallbacks) { + callback.onManagedProfileChanged(); + } + } + + @Override + public void onProfilesChanged(@NonNull List<UserInfo> profiles) { + reloadManagedProfiles(); + for (Callback callback : mCallbacks) { + callback.onManagedProfileChanged(); + } + } + }; } 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 fb0d3e4406bf..d500f999b5e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -352,6 +352,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump .getBoolean(R.bool.notification_scrim_transparent); updateScrims(); mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); + + // prepare() sets proper initial values for most states + for (ScrimState state : ScrimState.values()) { + state.prepare(state); + } } /** @@ -641,10 +646,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private void setTransitionToFullShade(boolean transitioning) { if (transitioning != mTransitioningToFullShade) { mTransitioningToFullShade = transitioning; - if (transitioning) { - // Let's make sure the shade locked is ready - ScrimState.SHADE_LOCKED.prepare(mState); - } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 52430d33cbf0..0e9d3ce33d5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -146,18 +146,12 @@ public enum ScrimState { mBehindAlpha = mClipQsScrim ? 1 : mDefaultScrimAlpha; mNotifAlpha = 1f; mFrontAlpha = 0f; - mBehindTint = Color.BLACK; + mBehindTint = mClipQsScrim ? Color.TRANSPARENT : Color.BLACK; if (mClipQsScrim) { updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK); } } - - // to make sure correct color is returned before "prepare" is called - @Override - public int getBehindTint() { - return Color.BLACK; - } }, /** 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 dcbabaac8a5b..3b160c85ca15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -469,6 +469,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb // Don't expand to the bouncer. Instead transition back to the lock screen (see // CentralSurfaces#showBouncerOrLockScreenIfKeyguard) return; + } else if (mKeyguardStateController.isOccluded() + && !mDreamOverlayStateController.isOverlayActive()) { + return; } else if (needsFullscreenBouncer()) { if (mPrimaryBouncer != null) { mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index b6ae4a088880..05bf8604c2c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -260,11 +260,11 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte if (showOverLockscreen) { mShadeController.addPostCollapseAction(runnable); - mShadeController.collapsePanel(true /* animate */); + mShadeController.collapseShade(true /* animate */); } else if (mKeyguardStateController.isShowing() && mCentralSurfaces.isOccluded()) { mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable); - mShadeController.collapsePanel(); + mShadeController.collapseShade(); } else { runnable.run(); } @@ -406,7 +406,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte private void expandBubbleStack(NotificationEntry entry) { mBubblesManagerOptional.get().expandStackAndSelectBubble(entry); - mShadeController.collapsePanel(); + mShadeController.collapseShade(); } private void startNotificationIntent( @@ -593,9 +593,9 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte private void collapseOnMainThread() { if (Looper.getMainLooper().isCurrentThread()) { - mShadeController.collapsePanel(); + mShadeController.collapseShade(); } else { - mMainThreadHandler.post(mShadeController::collapsePanel); + mMainThreadHandler.post(mShadeController::collapseShade); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 8a49850b1822..7fe01825890f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -180,7 +180,7 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, } }; mShadeController.postOnShadeExpanded(clickPendingViewRunnable); - mShadeController.instantExpandNotificationsPanel(); + mShadeController.instantExpandShade(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 946d7e4a3e75..4d914fe0adef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -52,6 +52,6 @@ class StatusBarPipelineFlags @Inject constructor(private val featureFlags: Featu * Returns true if we should apply some coloring to the wifi icon that was rendered with the new * pipeline to help with debugging. */ - // For now, just always apply the debug coloring if we've enabled the new icon. - fun useWifiDebugColoring(): Boolean = useNewWifiIcon() + fun useWifiDebugColoring(): Boolean = + featureFlags.isEnabled(Flags.NEW_STATUS_BAR_ICONS_DEBUG_COLORING) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt index 7aa5ee1389f3..8ff9198da119 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -23,9 +23,10 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton 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.logDiffsForTable import com.android.systemui.qs.SettingObserver -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.dagger.AirplaneTableLog import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -58,7 +59,7 @@ class AirplaneModeRepositoryImpl constructor( @Background private val bgHandler: Handler, private val globalSettings: GlobalSettings, - logger: ConnectivityPipelineLogger, + @AirplaneTableLog logger: TableLogBuffer, @Application scope: CoroutineScope, ) : AirplaneModeRepository { // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it. @@ -82,7 +83,12 @@ constructor( awaitClose { observer.isListening = false } } .distinctUntilChanged() - .logInputChange(logger, "isAirplaneMode") + .logDiffsForTable( + logger, + columnPrefix = "", + columnName = "isAirplaneMode", + initialValue = false + ) .stateIn( scope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt index fe30c0169021..4a5342e0f765 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt @@ -36,16 +36,20 @@ import kotlinx.coroutines.flow.stateIn * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for * more details. */ +interface AirplaneModeViewModel { + /** True if the airplane mode icon is currently visible in the status bar. */ + val isAirplaneModeIconVisible: StateFlow<Boolean> +} + @SysUISingleton -class AirplaneModeViewModel +class AirplaneModeViewModelImpl @Inject constructor( interactor: AirplaneModeInteractor, logger: ConnectivityPipelineLogger, @Application private val scope: CoroutineScope, -) { - /** True if the airplane mode icon is currently visible in the status bar. */ - val isAirplaneModeIconVisible: StateFlow<Boolean> = +) : AirplaneModeViewModel { + override val isAirplaneModeIconVisible: StateFlow<Boolean> = combine(interactor.isAirplaneMode, interactor.isForceHidden) { isAirplaneMode, isAirplaneIconForceHidden -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/AirplaneTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/AirplaneTableLog.kt new file mode 100644 index 000000000000..4f70f660187f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/AirplaneTableLog.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.statusbar.pipeline.dagger + +import javax.inject.Qualifier + +/** Airplane mode logs in table format. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class AirplaneTableLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 0662fb3d52b9..fb67f1a1bf50 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -21,6 +21,8 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository @@ -33,6 +35,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import dagger.Binds import dagger.Module import dagger.Provides @@ -43,12 +47,18 @@ abstract class StatusBarPipelineModule { abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository @Binds + abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds + abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor + + @Binds abstract fun mobileConnectionsRepository( impl: MobileConnectionsRepositoryImpl ): MobileConnectionsRepository @@ -71,5 +81,13 @@ abstract class StatusBarPipelineModule { fun provideWifiTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer { return factory.create("WifiTableLog", 100) } + + @JvmStatic + @Provides + @SysUISingleton + @AirplaneTableLog + fun provideAirplaneTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("AirplaneTableLog", 30) + } } } 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 8436b13d7038..a682a5711a6f 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 @@ -31,15 +31,19 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { if (prevVal is Inactive) { return } - row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) 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. - logDiffsFromActiveToNotActive(prevVal as Active, row) + logFullNonActiveNetwork(TYPE_INACTIVE, row) + } + + override fun logFull(row: TableRowLogger) { + logFullNonActiveNetwork(TYPE_INACTIVE, row) } } @@ -56,15 +60,15 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { if (prevVal is CarrierMerged) { return } - row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) if (prevVal is Inactive) { // The only difference between CarrierMerged and Inactive is the type. + row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) return } // When changing from Active to CarrierMerged, we need to log diffs to all the fields. - logDiffsFromActiveToNotActive(prevVal as Active, row) + logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row) } } @@ -121,7 +125,11 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { row.logChange(COL_VALIDATED, isValidated) } if (prevVal !is Active || prevVal.level != level) { - row.logChange(COL_LEVEL, level ?: LEVEL_DEFAULT) + if (level != null) { + row.logChange(COL_LEVEL, level) + } else { + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + } } if (prevVal !is Active || prevVal.ssid != ssid) { row.logChange(COL_SSID, ssid) @@ -143,7 +151,6 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { } } - 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.) @@ -170,21 +177,15 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { } } - internal fun logDiffsFromActiveToNotActive(prevActive: Active, row: TableRowLogger) { + 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) - - if (prevActive.isPasspointAccessPoint) { - row.logChange(COL_PASSPOINT_ACCESS_POINT, false) - } - if (prevActive.isOnlineSignUpForPasspointAccessPoint) { - row.logChange(COL_ONLINE_SIGN_UP, false) - } - if (prevActive.passpointProviderFriendlyName != null) { - row.logChange(COL_PASSPOINT_NAME, null) - } + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -201,5 +202,5 @@ const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint" const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint" const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName" -const val LEVEL_DEFAULT = -1 -const val NETWORK_ID_DEFAULT = -1 +val LEVEL_DEFAULT: String? = null +val NETWORK_ID_DEFAULT: String? = null 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 3a3e611de96a..ec935fe23d39 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 @@ -34,16 +34,36 @@ import kotlinx.coroutines.flow.map * This interactor processes information from our data layer into information that the UI layer can * use. */ -@SysUISingleton -class WifiInteractor @Inject constructor( - connectivityRepository: ConnectivityRepository, - wifiRepository: WifiRepository, -) { +interface WifiInteractor { /** * The SSID (service set identifier) of the wifi network. Null if we don't have a network, or * have a network but no valid SSID. */ - val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> + val ssid: Flow<String?> + + /** Our current enabled status. */ + val isEnabled: Flow<Boolean> + + /** Our current default status. */ + val isDefault: Flow<Boolean> + + /** Our current wifi network. See [WifiNetworkModel]. */ + val wifiNetwork: Flow<WifiNetworkModel> + + /** Our current wifi activity. See [WifiActivityModel]. */ + val activity: StateFlow<WifiActivityModel> + + /** True if we're configured to force-hide the wifi icon and false otherwise. */ + val isForceHidden: Flow<Boolean> +} + +@SysUISingleton +class WifiInteractorImpl @Inject constructor( + connectivityRepository: ConnectivityRepository, + wifiRepository: WifiRepository, +) : WifiInteractor { + + override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> when (info) { is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null @@ -56,20 +76,15 @@ class WifiInteractor @Inject constructor( } } - /** Our current enabled status. */ - val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled + override val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled - /** Our current default status. */ - val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault + override val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault - /** Our current wifi network. See [WifiNetworkModel]. */ - val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork + override val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork - /** Our current wifi activity. See [WifiActivityModel]. */ - val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity + override val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity - /** True if we're configured to force-hide the wifi icon and false otherwise. */ - val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map { + override val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.WIFI) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index d84cbcc60853..6875b523a962 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -120,6 +120,7 @@ public class Clock extends TextView implements @Override public void onUserChanged(int newUser, @NonNull Context userContext) { mCurrentUserId = newUser; + updateClock(); } }; @@ -190,7 +191,6 @@ public class Clock extends TextView implements filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); // NOTE: This receiver could run before this method returns, as it's not dispatching // on the main thread and BroadcastDispatcher may not need to register with Context. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java index b234e9c4e746..63b9ff9717d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java @@ -28,11 +28,14 @@ import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Date; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -45,22 +48,34 @@ public class NextAlarmControllerImpl extends BroadcastReceiver private final ArrayList<NextAlarmChangeCallback> mChangeCallbacks = new ArrayList<>(); + private final UserTracker mUserTracker; private AlarmManager mAlarmManager; private AlarmManager.AlarmClockInfo mNextAlarm; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + updateNextAlarm(); + } + }; + /** */ @Inject public NextAlarmControllerImpl( + @Main Executor mainExecutor, AlarmManager alarmManager, BroadcastDispatcher broadcastDispatcher, - DumpManager dumpManager) { + DumpManager dumpManager, + UserTracker userTracker) { dumpManager.registerDumpable("NextAlarmController", this); mAlarmManager = alarmManager; + mUserTracker = userTracker; IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL); + mUserTracker.addCallback(mUserChangedCallback, mainExecutor); updateNextAlarm(); } @@ -98,14 +113,13 @@ public class NextAlarmControllerImpl extends BroadcastReceiver public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); - if (action.equals(Intent.ACTION_USER_SWITCHED) - || action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { + if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { updateNextAlarm(); } } private void updateNextAlarm() { - mNextAlarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT); + mNextAlarm = mAlarmManager.getNextAlarmClock(mUserTracker.getUserId()); fireNextAlarmChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 0c72b78a3c46..2b29885db682 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -17,6 +17,7 @@ package com.android.systemui.user; import android.app.Activity; +import android.os.UserHandle; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; @@ -51,4 +52,22 @@ public abstract class UserModule { @IntoMap @ClassKey(UserSwitcherActivity.class) public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); + + /** + * Provides the {@link UserHandle} for the user associated with this System UI process. + * + * <p>Note that this is static and unchanging for the life-time of the process we are running + * in. It can be <i>different</i> from the user that is the currently-selected user, which may + * be associated with a different System UI process. + * + * <p>For example, the System UI process which creates all the windows and renders UI is always + * the one associated with the primary user on the device. However, if the user is switched to + * another, non-primary user (for example user "X"), then a secondary System UI process will be + * spawned. While the original primary user process continues to be the only one rendering UI, + * the new system UI process may be used for things like file or content access. + */ + @Provides + public static UserHandle provideUserHandle() { + return new UserHandle(UserHandle.myUserId()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 4c9b8e4639ca..c0f03902202a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -242,7 +242,15 @@ constructor( val isUserSwitcherEnabled = globalSettings.getIntForUser( Settings.Global.USER_SWITCHER_ENABLED, - 0, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_showUserSwitcherByDefault + ) + ) { + 1 + } else { + 0 + }, UserHandle.USER_SYSTEM, ) != 0 diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt index f71d596ff835..b61b2e66fb22 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt @@ -20,7 +20,6 @@ import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onStart @@ -58,6 +57,22 @@ fun <T, R> Flow<T>.pairwiseBy( onStart { emit(initialValue) }.pairwiseBy(transform) /** + * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform]. + * + * + * The output of [getInitialValue] will be used as the "old" value for the first emission. As + * opposed to the initial value in the above [pairwiseBy], [getInitialValue] can do some work before + * returning the initial value. + * + * Useful for code that needs to compare the current value to the previous value. + */ +fun <T, R> Flow<T>.pairwiseBy( + getInitialValue: suspend () -> T, + transform: suspend (previousValue: T, newValue: T) -> R, +): Flow<R> = + onStart { emit(getInitialValue()) }.pairwiseBy(transform) + +/** * Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new * Flow will not start emitting until it has received two emissions from the upstream Flow. * diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index a4384d5810ce..7033ccde8c7d 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -549,7 +549,7 @@ public class BubblesManager { } catch (RemoteException e) { Log.e(TAG, e.getMessage()); } - mShadeController.collapsePanel(true); + mShadeController.collapseShade(true); if (entry.getRow() != null) { entry.getRow().updateBubbleButton(); } @@ -597,7 +597,7 @@ public class BubblesManager { } if (shouldBubble) { - mShadeController.collapsePanel(true); + mShadeController.collapseShade(true); if (entry.getRow() != null) { entry.getRow().updateBubbleButton(); } diff --git a/packages/SystemUI/tests/robolectric/config/robolectric.properties b/packages/SystemUI/tests/robolectric/config/robolectric.properties new file mode 100644 index 000000000000..2a75bd98bfe8 --- /dev/null +++ b/packages/SystemUI/tests/robolectric/config/robolectric.properties @@ -0,0 +1,16 @@ +# +# 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. +# +sdk=NEWEST_SDK
\ No newline at end of file diff --git a/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiResourceLoadingTest.java b/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiResourceLoadingTest.java new file mode 100644 index 000000000000..188dff21efa4 --- /dev/null +++ b/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiResourceLoadingTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.robotests; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; +import static com.google.common.truth.Truth.assertThat; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SysuiResourceLoadingTest extends SysuiRoboBase { + @Test + public void testResources() { + assertThat(getContext().getString(com.android.systemui.R.string.app_label)) + .isEqualTo("System UI"); + assertThat(getContext().getString(com.android.systemui.tests.R.string.test_content)) + .isNotEmpty(); + } +} diff --git a/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiRoboBase.java b/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiRoboBase.java new file mode 100644 index 000000000000..d9686bbeb0cd --- /dev/null +++ b/packages/SystemUI/tests/robolectric/src/com/android/systemui/robotests/SysuiRoboBase.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.robotests; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; + +public class SysuiRoboBase { + public Context getContext() { + return InstrumentationRegistry.getContext(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt index 88396628017b..afd582a3b822 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt @@ -63,7 +63,6 @@ private fun fingerprintModel(user: Int) = KeyguardFingerprintListenModel( credentialAttempted = false, deviceInteractive = false, dreaming = false, - encryptedOrLockdown = false, fingerprintDisabled = false, fingerprintLockedOut = false, goingToSleep = false, @@ -74,6 +73,7 @@ private fun fingerprintModel(user: Int) = KeyguardFingerprintListenModel( primaryUser = false, shouldListenSfpsState = false, shouldListenForFingerprintAssistant = false, + strongerAuthRequired = false, switchingUser = false, udfps = false, userDoesNotHaveTrust = false diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 4d58b09f1076..84f6d913b310 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -379,9 +379,9 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test - public void onBouncerVisibilityChanged_needsStrongAuth_sideFpsHintHidden() { + public void onBouncerVisibilityChanged_unlockingWithFingerprintNotAllowed_sideFpsHintHidden() { setupConditionsToEnableSideFpsHint(); - setNeedsStrongAuth(true); + setUnlockingWithFingerprintAllowed(false); reset(mSideFpsController); mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE); @@ -558,11 +558,40 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onDensityOrFontScaleChanged(); verify(mView).onDensityOrFontScaleChanged(); - verify(mKeyguardSecurityViewFlipperController).onDensityOrFontScaleChanged(); + verify(mKeyguardSecurityViewFlipperController).clearViews(); verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class), any(KeyguardSecurityCallback.class)); } + @Test + public void onThemeChanged() { + ArgumentCaptor<ConfigurationController.ConfigurationListener> + configurationListenerArgumentCaptor = ArgumentCaptor.forClass( + ConfigurationController.ConfigurationListener.class); + mKeyguardSecurityContainerController.onViewAttached(); + verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture()); + configurationListenerArgumentCaptor.getValue().onThemeChanged(); + + verify(mView).reloadColors(); + verify(mKeyguardSecurityViewFlipperController).clearViews(); + verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class), + any(KeyguardSecurityCallback.class)); + } + + @Test + public void onUiModeChanged() { + ArgumentCaptor<ConfigurationController.ConfigurationListener> + configurationListenerArgumentCaptor = ArgumentCaptor.forClass( + ConfigurationController.ConfigurationListener.class); + mKeyguardSecurityContainerController.onViewAttached(); + verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture()); + configurationListenerArgumentCaptor.getValue().onUiModeChanged(); + + verify(mView).reloadColors(); + verify(mKeyguardSecurityViewFlipperController).clearViews(); + verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class), + any(KeyguardSecurityCallback.class)); + } private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { mKeyguardSecurityContainerController.onViewAttached(); @@ -574,7 +603,7 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { attachView(); setSideFpsHintEnabledFromResources(true); setFingerprintDetectionRunning(true); - setNeedsStrongAuth(false); + setUnlockingWithFingerprintAllowed(true); } private void attachView() { @@ -593,9 +622,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { enabled); } - private void setNeedsStrongAuth(boolean needed) { - when(mKeyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(needed); - mKeyguardUpdateMonitorCallback.getValue().onStrongAuthStateChanged(/* userId= */ 0); + private void setUnlockingWithFingerprintAllowed(boolean allowed) { + when(mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()).thenReturn(allowed); } private void setupGetSecurityView() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java index fd02ac97cec2..1614b577a6cc 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java @@ -109,7 +109,7 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase { @Test public void onDensityOrFontScaleChanged() { - mKeyguardSecurityViewFlipperController.onDensityOrFontScaleChanged(); + mKeyguardSecurityViewFlipperController.clearViews(); verify(mView).removeAllViews(); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index beb9a72de324..c87a66467608 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -27,6 +27,7 @@ import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; +import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELLING_RESTARTING; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; @@ -281,7 +282,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { componentInfo, FaceSensorProperties.TYPE_UNKNOWN, false /* supportsFaceDetection */, true /* supportsSelfIllumination */, false /* resetLockoutRequiresChallenge */)); - when(mFingerprintManager.isHardwareDetected()).thenReturn(true); when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(List.of( @@ -594,30 +594,13 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testFingerprintDoesNotAuth_whenEncrypted() { - testFingerprintWhenStrongAuth( - STRONG_AUTH_REQUIRED_AFTER_BOOT); - } - - @Test - public void testFingerprintDoesNotAuth_whenDpmLocked() { - testFingerprintWhenStrongAuth( - KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW); - } - - @Test - public void testFingerprintDoesNotAuth_whenUserLockdown() { - testFingerprintWhenStrongAuth( - KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - } - - private void testFingerprintWhenStrongAuth(int strongAuth) { + public void testOnlyDetectFingerprint_whenFingerprintUnlockNotAllowed() { // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks) // will trigger updateBiometricListeningState(); clearInvocations(mFingerprintManager); mKeyguardUpdateMonitor.resetBiometricListeningState(); - when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth); + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); mTestableLooper.processAllMessages(); @@ -626,6 +609,64 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testUnlockingWithFaceAllowed_strongAuthTrackerUnlockingWithBiometricAllowed() { + // GIVEN unlocking with biometric is allowed + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + + // THEN unlocking with face and fp is allowed + Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)); + Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test + public void testUnlockingWithFaceAllowed_strongAuthTrackerUnlockingWithBiometricNotAllowed() { + // GIVEN unlocking with biometric is not allowed + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); + + // THEN unlocking with face is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)); + } + + @Test + public void testUnlockingWithFaceAllowed_fingerprintLockout() { + // GIVEN unlocking with biometric is allowed + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + + // WHEN fingerprint is locked out + fingerprintErrorLockedOut(); + + // THEN unlocking with face is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)); + } + + @Test + public void testUnlockingWithFpAllowed_strongAuthTrackerUnlockingWithBiometricNotAllowed() { + // GIVEN unlocking with biometric is not allowed + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); + + // THEN unlocking with fingerprint is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test + public void testUnlockingWithFpAllowed_fingerprintLockout() { + // GIVEN unlocking with biometric is allowed + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + + // WHEN fingerprint is locked out + fingerprintErrorLockedOut(); + + // THEN unlocking with fingeprint is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test public void testTriesToAuthenticate_whenBouncer() { setKeyguardBouncerVisibility(true); @@ -928,10 +969,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { faceLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; final boolean fpLocked = fingerprintLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; - when(mFingerprintManager.getLockoutModeForUser(eq(FINGERPRINT_SENSOR_ID), eq(newUser))) - .thenReturn(fingerprintLockoutMode); - when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser))) - .thenReturn(faceLockoutMode); mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); @@ -940,7 +977,13 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(), anyInt()); +// resetFaceManager(); +// resetFingerprintManager(); + when(mFingerprintManager.getLockoutModeForUser(eq(FINGERPRINT_SENSOR_ID), eq(newUser))) + .thenReturn(fingerprintLockoutMode); + when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser))) + .thenReturn(faceLockoutMode); final CancellationSignal faceCancel = spy(mKeyguardUpdateMonitor.mFaceCancelSignal); final CancellationSignal fpCancel = spy(mKeyguardUpdateMonitor.mFingerprintCancelSignal); mKeyguardUpdateMonitor.mFaceCancelSignal = faceCancel; @@ -951,14 +994,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.handleUserSwitchComplete(newUser); mTestableLooper.processAllMessages(); - verify(faceCancel, faceLocked ? times(1) : never()).cancel(); - verify(fpCancel, fpLocked ? times(1) : never()).cancel(); - verify(callback, faceLocked ? times(1) : never()).onBiometricRunningStateChanged( + // THEN face and fingerprint listening are always cancelled immediately + verify(faceCancel).cancel(); + verify(callback).onBiometricRunningStateChanged( eq(false), eq(BiometricSourceType.FACE)); - verify(callback, fpLocked ? times(1) : never()).onBiometricRunningStateChanged( + verify(fpCancel).cancel(); + verify(callback).onBiometricRunningStateChanged( eq(false), eq(BiometricSourceType.FINGERPRINT)); + + // THEN locked out states are updated assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLocked); assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLocked); + + // Fingerprint should be restarted once its cancelled bc on lockout, the device + // can still detectFingerprint (and if it's not locked out, fingerprint can listen) + assertThat(mKeyguardUpdateMonitor.mFingerprintRunningState) + .isEqualTo(BIOMETRIC_STATE_CANCELLING_RESTARTING); } @Test @@ -1144,9 +1195,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // GIVEN status bar state is on the keyguard mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD); - // WHEN user hasn't authenticated since last boot - when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) - .thenReturn(STRONG_AUTH_REQUIRED_AFTER_BOOT); + // WHEN user hasn't authenticated since last boot, cannot unlock with FP + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); // THEN we shouldn't listen for udfps assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(false); @@ -1259,8 +1309,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true); // WHEN device in lock down - when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( - KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); // THEN we shouldn't listen for udfps assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(false); @@ -1294,6 +1343,24 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testRequestFaceAuthFromOccludingApp_whenInvoked_startsFaceAuth() { + mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true); + + assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isTrue(); + } + + @Test + public void testRequestFaceAuthFromOccludingApp_whenInvoked_stopsFaceAuth() { + mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true); + + assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isTrue(); + + mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false); + + assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isFalse(); + } + + @Test public void testRequireUnlockForNfc_Broadcast() { KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); mKeyguardUpdateMonitor.registerCallback(callback); diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 181839ab512f..0627fc6c542f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -77,7 +77,6 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.decor.CornerDecorProvider; import com.android.systemui.decor.CutoutDecorProviderFactory; import com.android.systemui.decor.CutoutDecorProviderImpl; @@ -132,8 +131,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { @Mock private TunerService mTunerService; @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock private UserTracker mUserTracker; @Mock private PrivacyDotViewController mDotViewController; @@ -223,8 +220,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { mExecutor)); mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings, - mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController, - mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) { + mTunerService, mUserTracker, mDotViewController, mThreadFactory, + mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) { @Override public void start() { super.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index 0b528a5c8d1e..eb8c823ffe1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -37,7 +37,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.leak.RotationUtils -import javax.inject.Provider +import com.android.systemui.util.mockito.any import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -46,15 +46,16 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.any +import org.mockito.Mockito.`when` import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.MockitoSession import org.mockito.quality.Strictness +import javax.inject.Provider @SmallTest @RunWith(AndroidTestingRunner::class) @@ -118,12 +119,13 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testFingerprintTrigger_KeyguardShowing_Ripple() { - // GIVEN fp exists, keyguard is showing, user doesn't need strong auth + // GIVEN fp exists, keyguard is showing, unlocking with fp allowed val fpsLocation = Point(5, 5) `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) // WHEN fingerprint authenticated val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) @@ -140,11 +142,12 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testFingerprintTrigger_KeyguardNotShowing_NoRipple() { - // GIVEN fp exists & user doesn't need strong auth + // GIVEN fp exists & unlocking with fp allowed val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) // WHEN keyguard is NOT showing & fingerprint authenticated `when`(keyguardStateController.isShowing).thenReturn(false) @@ -160,15 +163,16 @@ class AuthRippleControllerTest : SysuiTestCase() { } @Test - fun testFingerprintTrigger_StrongAuthRequired_NoRipple() { + fun testFingerprintTrigger_biometricUnlockNotAllowed_NoRipple() { // GIVEN fp exists & keyguard is showing val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - // WHEN user needs strong auth & fingerprint authenticated - `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(true) + // WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT))).thenReturn(false) val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) verify(keyguardUpdateMonitor).registerCallback(captor.capture()) captor.value.onBiometricAuthenticated( @@ -182,13 +186,14 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testFaceTriggerBypassEnabled_Ripple() { - // GIVEN face auth sensor exists, keyguard is showing & strong auth isn't required + // GIVEN face auth sensor exists, keyguard is showing & unlocking with face is allowed val faceLocation = Point(5, 5) `when`(authController.faceSensorLocation).thenReturn(faceLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)).thenReturn(true) // WHEN bypass is enabled & face authenticated `when`(bypassController.canBypass()).thenReturn(true) @@ -275,6 +280,8 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FINGERPRINT) @@ -295,6 +302,8 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) `when`(authController.isUdfpsFingerDown).thenReturn(true) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FACE))).thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FACE) assertTrue("reveal didn't start on keyguardFadingAway", 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 acdafe3e1c7d..b267a5c23a49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -70,8 +70,13 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.biometrics.udfps.InteractionEvent; +import com.android.systemui.biometrics.udfps.NormalizedTouchData; +import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor; +import com.android.systemui.biometrics.udfps.TouchProcessorResult; 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.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; @@ -190,6 +195,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private AlternateUdfpsTouchProvider mAlternateTouchProvider; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock + private SinglePointerTouchProcessor mSinglePointerTouchProcessor; // Capture listeners so that they can be used to send events @Captor @@ -275,7 +282,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mDisplayManager, mHandler, mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricsExecutor, - mPrimaryBouncerInteractor); + mPrimaryBouncerInteractor, mSinglePointerTouchProcessor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -1086,4 +1093,100 @@ public class UdfpsControllerTest extends SysuiTestCase { anyString(), any()); } + + @Test + public void onTouch_withoutNewTouchDetection_shouldCallOldFingerprintManagerPath() + throws RemoteException { + // Disable new touch detection. + when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(false); + + // Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider. + initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */); + + // Configure UdfpsView to accept the ACTION_DOWN event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); + + // GIVEN that the overlay is showing and a11y touch exploration NOT enabled + when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); + + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + + // WHEN ACTION_DOWN is received + MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); + mBiometricsExecutor.runAllReady(); + downEvent.recycle(); + + // AND ACTION_MOVE is received + MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); + mBiometricsExecutor.runAllReady(); + moveEvent.recycle(); + + // AND ACTION_UP is received + MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); + mBiometricsExecutor.runAllReady(); + upEvent.recycle(); + + // THEN the old FingerprintManager path is invoked. + verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), + anyFloat(), anyFloat()); + verify(mFingerprintManager).onPointerUp(anyLong(), anyInt()); + } + + @Test + public void onTouch_withNewTouchDetection_shouldCallOldFingerprintManagerPath() + throws RemoteException { + final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, + 0L); + final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch( + InteractionEvent.DOWN, 1 /* pointerId */, touchData); + final TouchProcessorResult processorResultUp = new TouchProcessorResult.ProcessedTouch( + InteractionEvent.UP, 1 /* pointerId */, touchData); + + // Enable new touch detection. + when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true); + + // Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider. + initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */); + + // Configure UdfpsView to accept the ACTION_DOWN event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); + + // GIVEN that the overlay is showing and a11y touch exploration NOT enabled + when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); + + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + + // WHEN ACTION_DOWN is received + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultDown); + MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); + mBiometricsExecutor.runAllReady(); + downEvent.recycle(); + + // AND ACTION_UP is received + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultUp); + MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent); + mBiometricsExecutor.runAllReady(); + upEvent.recycle(); + + // THEN the new FingerprintManager path is invoked. + verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(), + anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); + verify(mFingerprintManager).onPointerUp(anyLong(), anyInt(), anyInt(), anyFloat(), + anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt new file mode 100644 index 000000000000..4f89b69108f4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt @@ -0,0 +1,109 @@ +/* + * 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.biometrics.udfps + +import android.graphics.Rect +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 +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { + val underTest = BoundingBoxOverlapDetector() + + @Test + fun isGoodOverlap() { + val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) + val actual = underTest.isGoodOverlap(touchData, SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase(val x: Int, val y: Int, val expected: Boolean) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genPositiveTestCases( + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ), + genNegativeTestCases( + invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ) + ) + .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 = 1.23f +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 */, 500 /* bottom */) + +private fun genTestCases( + xs: List<Int>, + ys: List<Int>, + expected: Boolean +): List<BoundingBoxOverlapDetectorTest.TestCase> { + return xs.flatMap { x -> + ys.map { y -> BoundingBoxOverlapDetectorTest.TestCase(x, y, expected) } + } +} + +private fun genPositiveTestCases( + validXs: List<Int>, + validYs: List<Int>, +) = genTestCases(validXs, validYs, expected = true) + +private fun genNegativeTestCases( + invalidXs: List<Int>, + invalidYs: List<Int>, + validXs: List<Int>, + validYs: List<Int>, +): List<BoundingBoxOverlapDetectorTest.TestCase> { + return genTestCases(invalidXs, validYs, expected = false) + + genTestCases(validXs, invalidYs, expected = false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt new file mode 100644 index 000000000000..834d0a69e427 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt @@ -0,0 +1,90 @@ +package com.android.systemui.biometrics.udfps + +import android.graphics.Rect +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 +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class NormalizedTouchDataTest(val testCase: TestCase) : SysuiTestCase() { + + @Test + fun isWithinSensor() { + val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) + val actual = touchData.isWithinSensor(SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase(val x: Int, val y: Int, val expected: Boolean) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genPositiveTestCases( + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ), + genNegativeTestCases( + invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) + ) + ) + .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 = 1.23f +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 */, 500 /* bottom */) + +private fun genTestCases( + xs: List<Int>, + ys: List<Int>, + expected: Boolean +): List<NormalizedTouchDataTest.TestCase> { + return xs.flatMap { x -> ys.map { y -> NormalizedTouchDataTest.TestCase(x, y, expected) } } +} + +private fun genPositiveTestCases( + validXs: List<Int>, + validYs: List<Int>, +) = genTestCases(validXs, validYs, expected = true) + +private fun genNegativeTestCases( + invalidXs: List<Int>, + invalidYs: List<Int>, + validXs: List<Int>, + validYs: List<Int>, +): List<NormalizedTouchDataTest.TestCase> { + return genTestCases(invalidXs, validYs, expected = false) + + genTestCases(validXs, invalidYs, expected = false) +} 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 new file mode 100644 index 000000000000..95c53b408056 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -0,0 +1,506 @@ +/* + * 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.biometrics.udfps + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.MotionEvent.INVALID_POINTER_ID +import android.view.MotionEvent.PointerProperties +import android.view.Surface +import android.view.Surface.Rotation +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.UdfpsOverlayParams +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() { + private val overlapDetector = FakeOverlapDetector() + private val underTest = SinglePointerTouchProcessor(overlapDetector) + + @Test + fun processTouch() { + overlapDetector.shouldReturn = testCase.isGoodOverlap + + val actual = + underTest.processTouch( + testCase.event, + testCase.previousPointerOnSensorId, + testCase.overlayParams, + ) + + assertThat(actual).isInstanceOf(testCase.expected.javaClass) + if (actual is TouchProcessorResult.ProcessedTouch) { + assertThat(actual).isEqualTo(testCase.expected) + } + } + + data class TestCase( + val event: MotionEvent, + val isGoodOverlap: Boolean, + val previousPointerOnSensorId: Int, + val overlayParams: UdfpsOverlayParams, + val expected: TouchProcessorResult, + ) { + override fun toString(): String { + val expectedOutput = + if (expected is TouchProcessorResult.ProcessedTouch) { + expected.event.toString() + + ", (x: ${expected.touchData.x}, y: ${expected.touchData.y})" + + ", pointerOnSensorId: ${expected.pointerOnSensorId}" + + ", ..." + } else { + TouchProcessorResult.Failure().toString() + } + return "{" + + MotionEvent.actionToString(event.action) + + ", (x: ${event.x}, y: ${event.y})" + + ", scale: ${overlayParams.scaleFactor}" + + ", rotation: " + + Surface.rotationToString(overlayParams.rotation) + + ", previousPointerOnSensorId: $previousPointerOnSensorId" + + ", ...} expected: {$expectedOutput}" + } + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + // MotionEvent.ACTION_DOWN + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_DOWN, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + // MotionEvent.ACTION_MOVE + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.DOWN, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UNCHANGED, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_MOVE, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + // MotionEvent.ACTION_UP + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.UP, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_UP, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = 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, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = true, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = INVALID_POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + genPositiveTestCases( + motionEventAction = MotionEvent.ACTION_CANCEL, + previousPointerOnSensorId = POINTER_ID, + isGoodOverlap = false, + expectedInteractionEvent = InteractionEvent.CANCEL, + expectedPointerOnSensorId = INVALID_POINTER_ID, + ), + ) + .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), + ) + .flatten() + } +} + +/* 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 + +/* + * ROTATION_0 map: + * _ _ _ _ + * _ _ O _ + * _ _ _ _ + * _ S _ _ + * _ S _ _ + * _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_0_NATIVE_SENSOR_BOUNDS = + Rect( + 100, /* left */ + 300, /* top */ + 200, /* right */ + 500, /* bottom */ + ) +private val ROTATION_0_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_0, + nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), + nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), + nativeXOutsideSensor = 250f, + nativeYOutsideSensor = 150f, + ) + +/* + * ROTATION_90 map: + * _ _ _ _ _ _ + * _ O _ _ _ _ + * _ _ _ S S _ + * _ _ _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_90_NATIVE_SENSOR_BOUNDS = + Rect( + 300, /* left */ + 200, /* top */ + 500, /* right */ + 300, /* bottom */ + ) +private val ROTATION_90_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_90, + nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), + nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), + nativeXOutsideSensor = 150f, + nativeYOutsideSensor = 150f, + ) + +/* ROTATION_180 is not supported. It's treated the same as ROTATION_0. */ +private val ROTATION_180_INPUTS = + ROTATION_0_INPUTS.copy( + rotation = Surface.ROTATION_180, + ) + +/* + * ROTATION_270 map: + * _ _ _ _ _ _ + * _ S S _ _ _ + * _ _ _ _ O _ + * _ _ _ _ _ _ + * + * (_) empty space + * (S) sensor + * (O) touch outside of the sensor + */ +private val ROTATION_270_NATIVE_SENSOR_BOUNDS = + Rect( + 100, /* left */ + 100, /* top */ + 300, /* right */ + 200, /* bottom */ + ) +private val ROTATION_270_INPUTS = + OrientationBasedInputs( + rotation = Surface.ROTATION_270, + 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, + x = 0f, + y = 0f, + minor = 0f, + major = 0f, + orientation = ORIENTATION, + time = TIME, + gestureStart = GESTURE_START, + ) + +/* Template [NormalizedTouchData]. */ +private val NORMALIZED_TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +/* + * Contains test inputs that are tied to a particular device orientation. + * + * "native" means in native resolution (not scaled). + */ +private data class OrientationBasedInputs( + @Rotation val rotation: Int, + val nativeXWithinSensor: Float, + val nativeYWithinSensor: Float, + val nativeXOutsideSensor: Float, + val nativeYOutsideSensor: Float, +) { + + fun toOverlayParams(scaleFactor: Float): UdfpsOverlayParams = + UdfpsOverlayParams( + sensorBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor), + overlayBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor), + naturalDisplayHeight = (ROTATION_0_NATIVE_DISPLAY_HEIGHT * scaleFactor).toInt(), + naturalDisplayWidth = (ROTATION_0_NATIVE_DISPLAY_WIDTH * scaleFactor).toInt(), + scaleFactor = scaleFactor, + rotation = rotation + ) + + fun getNativeX(isWithinSensor: Boolean): Float { + return if (isWithinSensor) nativeXWithinSensor else nativeXOutsideSensor + } + + fun getNativeY(isWithinSensor: Boolean): Float { + return if (isWithinSensor) nativeYWithinSensor else nativeYOutsideSensor + } +} + +private fun genPositiveTestCases( + motionEventAction: Int, + previousPointerOnSensorId: Int, + isGoodOverlap: Boolean, + expectedInteractionEvent: InteractionEvent, + expectedPointerOnSensorId: Int +): List<SinglePointerTouchProcessorTest.TestCase> { + val scaleFactors = listOf(0.75f, 1f, 1.5f) + val orientations = + listOf( + ROTATION_0_INPUTS, + ROTATION_90_INPUTS, + ROTATION_180_INPUTS, + ROTATION_270_INPUTS, + ) + return scaleFactors.flatMap { scaleFactor -> + orientations.map { orientation -> + val overlayParams = orientation.toOverlayParams(scaleFactor) + val nativeX = orientation.getNativeX(isGoodOverlap) + val nativeY = orientation.getNativeY(isGoodOverlap) + val event = + MOTION_EVENT.copy( + action = motionEventAction, + x = nativeX * scaleFactor, + y = nativeY * scaleFactor, + minor = NATIVE_MINOR * scaleFactor, + major = NATIVE_MAJOR * scaleFactor, + ) + val expectedTouchData = + NORMALIZED_TOUCH_DATA.copy( + x = ROTATION_0_INPUTS.getNativeX(isGoodOverlap), + y = ROTATION_0_INPUTS.getNativeY(isGoodOverlap), + ) + val expected = + TouchProcessorResult.ProcessedTouch( + event = expectedInteractionEvent, + pointerOnSensorId = expectedPointerOnSensorId, + touchData = expectedTouchData, + ) + SinglePointerTouchProcessorTest.TestCase( + event = event, + isGoodOverlap = isGoodOverlap, + previousPointerOnSensorId = previousPointerOnSensorId, + overlayParams = overlayParams, + expected = expected, + ) + } + } +} + +private fun genTestCasesForUnsupportedAction( + motionEventAction: Int +): List<SinglePointerTouchProcessorTest.TestCase> { + val isGoodOverlap = true + val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID) + return previousPointerOnSensorIds.map { previousPointerOnSensorId -> + val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f) + val nativeX = ROTATION_0_INPUTS.getNativeX(isGoodOverlap) + val nativeY = ROTATION_0_INPUTS.getNativeY(isGoodOverlap) + val event = + MOTION_EVENT.copy( + action = motionEventAction, + x = nativeX, + y = nativeY, + minor = NATIVE_MINOR, + major = NATIVE_MAJOR, + ) + SinglePointerTouchProcessorTest.TestCase( + event = event, + isGoodOverlap = isGoodOverlap, + previousPointerOnSensorId = previousPointerOnSensorId, + overlayParams = overlayParams, + expected = TouchProcessorResult.Failure(), + ) + } +} + +private fun obtainMotionEvent( + action: Int, + pointerId: Int, + x: Float, + y: Float, + minor: Float, + major: Float, + orientation: Float, + time: Long, + gestureStart: Long, +): MotionEvent { + val pp = PointerProperties() + pp.id = pointerId + val pc = MotionEvent.PointerCoords() + pc.x = x + pc.y = y + pc.touchMinor = minor + pc.touchMajor = major + pc.orientation = orientation + return MotionEvent.obtain( + gestureStart /* downTime */, + time /* eventTime */, + action /* action */, + 1 /* pointerCount */, + arrayOf(pp) /* pointerProperties */, + arrayOf(pc) /* pointerCoords */, + 0 /* metaState */, + 0 /* buttonState */, + 1f /* xPrecision */, + 1f /* yPrecision */, + 0 /* deviceId */, + 0 /* edgeFlags */, + 0 /* source */, + 0 /* flags */ + ) +} + +private fun MotionEvent.copy( + action: Int = this.action, + pointerId: Int = this.getPointerId(0), + x: Float = this.rawX, + y: Float = this.rawY, + minor: Float = this.touchMinor, + major: Float = this.touchMajor, + orientation: Float = this.orientation, + time: Long = this.eventTime, + gestureStart: Long = this.downTime, +) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart) + +private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt new file mode 100644 index 000000000000..4b88b44c3f03 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt @@ -0,0 +1,187 @@ +/* + * 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.controls + +import android.content.pm.UserInfo +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +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.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class ControlsSettingsRepositoryImplTest : SysuiTestCase() { + + companion object { + private const val LOCKSCREEN_SHOW = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS + private const val LOCKSCREEN_ACTION = Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS + + private fun createUser(id: Int): UserInfo { + return UserInfo(id, "user_$id", 0) + } + + private val ALL_USERS = (0..1).map { it to createUser(it) }.toMap() + } + + private lateinit var underTest: ControlsSettingsRepository + + private lateinit var testScope: TestScope + private lateinit var secureSettings: FakeSettings + private lateinit var userRepository: FakeUserRepository + + @Before + fun setUp() { + secureSettings = FakeSettings() + userRepository = FakeUserRepository() + userRepository.setUserInfos(ALL_USERS.values.toList()) + + val coroutineDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(coroutineDispatcher) + + underTest = + ControlsSettingsRepositoryImpl( + scope = testScope.backgroundScope, + backgroundDispatcher = coroutineDispatcher, + userRepository = userRepository, + secureSettings = secureSettings, + ) + } + + @Test + fun showInLockScreen() = + testScope.runTest { + setUser(0) + val values = mutableListOf<Boolean>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.canShowControlsInLockscreen.toList(values) + } + assertThat(values.last()).isFalse() + + secureSettings.putBool(LOCKSCREEN_SHOW, true) + assertThat(values.last()).isTrue() + + secureSettings.putBool(LOCKSCREEN_SHOW, false) + assertThat(values.last()).isFalse() + + secureSettings.putBoolForUser(LOCKSCREEN_SHOW, true, 1) + assertThat(values.last()).isFalse() + + setUser(1) + assertThat(values.last()).isTrue() + + job.cancel() + } + + @Test + fun showInLockScreen_changesInOtherUsersAreNotQueued() = + testScope.runTest { + setUser(0) + + val values = mutableListOf<Boolean>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.canShowControlsInLockscreen.toList(values) + } + + secureSettings.putBoolForUser(LOCKSCREEN_SHOW, true, 1) + secureSettings.putBoolForUser(LOCKSCREEN_SHOW, false, 1) + + setUser(1) + assertThat(values.last()).isFalse() + assertThat(values).containsNoneIn(listOf(true)) + + job.cancel() + } + + @Test + fun actionInLockScreen() = + testScope.runTest { + setUser(0) + val values = mutableListOf<Boolean>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.allowActionOnTrivialControlsInLockscreen.toList(values) + } + assertThat(values.last()).isFalse() + + secureSettings.putBool(LOCKSCREEN_ACTION, true) + assertThat(values.last()).isTrue() + + secureSettings.putBool(LOCKSCREEN_ACTION, false) + assertThat(values.last()).isFalse() + + secureSettings.putBoolForUser(LOCKSCREEN_ACTION, true, 1) + assertThat(values.last()).isFalse() + + setUser(1) + assertThat(values.last()).isTrue() + + job.cancel() + } + + @Test + fun actionInLockScreen_changesInOtherUsersAreNotQueued() = + testScope.runTest { + setUser(0) + + val values = mutableListOf<Boolean>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.allowActionOnTrivialControlsInLockscreen.toList(values) + } + + secureSettings.putBoolForUser(LOCKSCREEN_ACTION, true, 1) + secureSettings.putBoolForUser(LOCKSCREEN_ACTION, false, 1) + + setUser(1) + assertThat(values.last()).isFalse() + assertThat(values).containsNoneIn(listOf(true)) + + job.cancel() + } + + @Test + fun valueIsUpdatedWhenNotSubscribed() = + testScope.runTest { + setUser(0) + assertThat(underTest.canShowControlsInLockscreen.value).isFalse() + + secureSettings.putBool(LOCKSCREEN_SHOW, true) + + assertThat(underTest.canShowControlsInLockscreen.value).isTrue() + } + + private suspend fun setUser(id: Int) { + secureSettings.userId = id + userRepository.setSelectedUserInfo(ALL_USERS[id]!!) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt new file mode 100644 index 000000000000..8a1bed20e700 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeControlsSettingsRepository : ControlsSettingsRepository { + private val _canShowControlsInLockscreen = MutableStateFlow(false) + override val canShowControlsInLockscreen = _canShowControlsInLockscreen.asStateFlow() + private val _allowActionOnTrivialControlsInLockscreen = MutableStateFlow(false) + override val allowActionOnTrivialControlsInLockscreen = + _allowActionOnTrivialControlsInLockscreen.asStateFlow() + + fun setCanShowControlsInLockscreen(value: Boolean) { + _canShowControlsInLockscreen.value = value + } + + fun setAllowActionOnTrivialControlsInLockscreen(value: Boolean) { + _allowActionOnTrivialControlsInLockscreen.value = value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 4ed5649c9c50..1d00d6b05568 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -18,30 +18,24 @@ package com.android.systemui.controls.ui import android.content.Context import android.content.SharedPreferences -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.UserHandle -import android.provider.Settings import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger +import com.android.systemui.controls.FakeControlsSettingsRepository import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers -import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` @@ -79,8 +73,6 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { @Mock private lateinit var secureSettings: SecureSettings @Mock - private lateinit var mainHandler: Handler - @Mock private lateinit var userContextProvider: UserContextProvider companion object { @@ -91,17 +83,15 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { private lateinit var coordinator: ControlActionCoordinatorImpl private lateinit var action: ControlActionCoordinatorImpl.Action + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository @Before fun setUp() { MockitoAnnotations.initMocks(this) - `when`(secureSettings.getUriFor(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS)) - .thenReturn(Settings.Secure - .getUriFor(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS)) - `when`(secureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, - 0, UserHandle.USER_CURRENT)) - .thenReturn(1) + controlsSettingsRepository = FakeControlsSettingsRepository() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + controlsSettingsRepository.setCanShowControlsInLockscreen(true) coordinator = spy(ControlActionCoordinatorImpl( mContext, @@ -115,7 +105,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { vibratorHelper, secureSettings, userContextProvider, - mainHandler + controlsSettingsRepository )) val userContext = mock(Context::class.java) @@ -128,9 +118,6 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(pref.getInt(DeviceControlsControllerImpl.PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) .thenReturn(2) - verify(secureSettings).registerContentObserverForUser(any(Uri::class.java), - anyBoolean(), any(ContentObserver::class.java), anyInt()) - `when`(cvh.cws.ci.controlId).thenReturn(ID) `when`(cvh.cws.control?.isAuthRequired()).thenReturn(true) action = spy(coordinator.Action(ID, {}, false, true)) 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 c31fd828c730..1b34706bd220 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 @@ -17,7 +17,6 @@ package com.android.systemui.controls.controller import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.ContextWrapper @@ -31,7 +30,6 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.backup.BackupHelper -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController @@ -85,10 +83,8 @@ class ControlsControllerImplTest : SysuiTestCase() { @Mock private lateinit var auxiliaryPersistenceWrapper: AuxiliaryPersistenceWrapper @Mock - private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var listingController: ControlsListingController - @Mock(stubOnly = true) + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userFileManager: UserFileManager @@ -104,7 +100,7 @@ class ControlsControllerImplTest : SysuiTestCase() { ArgumentCaptor<ControlsBindingController.LoadCallback> @Captor - private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver> + private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback> @Captor private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> @@ -170,16 +166,15 @@ class ControlsControllerImplTest : SysuiTestCase() { uiController, bindingController, listingController, - broadcastDispatcher, userFileManager, + userTracker, Optional.of(persistenceWrapper), - mock(DumpManager::class.java), - userTracker + mock(DumpManager::class.java) ) controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper - verify(broadcastDispatcher).registerReceiver( - capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL), anyInt(), any() + verify(userTracker).addCallback( + capture(userTrackerCallbackCaptor), any() ) verify(listingController).addCallback(capture(listingCallbackCaptor)) @@ -227,11 +222,10 @@ class ControlsControllerImplTest : SysuiTestCase() { uiController, bindingController, listingController, - broadcastDispatcher, userFileManager, + userTracker, Optional.of(persistenceWrapper), - mock(DumpManager::class.java), - userTracker + mock(DumpManager::class.java) ) assertEquals(listOf(TEST_STRUCTURE_INFO), controller_other.getFavorites()) } @@ -518,14 +512,8 @@ class ControlsControllerImplTest : SysuiTestCase() { delayableExecutor.runAllReady() reset(persistenceWrapper) - val intent = Intent(Intent.ACTION_USER_SWITCHED).apply { - putExtra(Intent.EXTRA_USER_HANDLE, otherUser) - } - val pendingResult = mock(BroadcastReceiver.PendingResult::class.java) - `when`(pendingResult.sendingUserId).thenReturn(otherUser) - broadcastReceiverCaptor.value.pendingResult = pendingResult - broadcastReceiverCaptor.value.onReceive(mContext, intent) + userTrackerCallbackCaptor.value.onUserChanged(otherUser, mContext) verify(persistenceWrapper).changeFileAndBackupManager(any(), any()) verify(persistenceWrapper).readFavorites() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt index 77f451f61c3c..48fc46b7e730 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt @@ -17,19 +17,18 @@ package com.android.systemui.controls.dagger import android.testing.AndroidTestingRunner -import android.provider.Settings 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.controls.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.settings.SecureSettings import dagger.Lazy import java.util.Optional import org.junit.Assert.assertEquals @@ -63,13 +62,13 @@ class ControlsComponentTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock - private lateinit var secureSettings: SecureSettings - @Mock private lateinit var optionalControlsTileResourceConfiguration: Optional<ControlsTileResourceConfiguration> @Mock private lateinit var controlsTileResourceConfiguration: ControlsTileResourceConfiguration + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + companion object { fun <T> eq(value: T): T = Mockito.eq(value) ?: value } @@ -78,6 +77,8 @@ class ControlsComponentTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + controlsSettingsRepository = FakeControlsSettingsRepository() + `when`(userTracker.userHandle.identifier).thenReturn(0) `when`(optionalControlsTileResourceConfiguration.orElse(any())) .thenReturn(controlsTileResourceConfiguration) @@ -125,8 +126,7 @@ class ControlsComponentTest : SysuiTestCase() { `when`(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(STRONG_AUTH_NOT_REQUIRED) `when`(keyguardStateController.isUnlocked()).thenReturn(false) - `when`(secureSettings.getInt(eq(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS), anyInt())) - .thenReturn(0) + controlsSettingsRepository.setCanShowControlsInLockscreen(false) val component = setupComponent(true) assertEquals(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK, component.getVisibility()) @@ -137,9 +137,7 @@ class ControlsComponentTest : SysuiTestCase() { `when`(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(STRONG_AUTH_NOT_REQUIRED) `when`(keyguardStateController.isUnlocked()).thenReturn(false) - `when`(secureSettings.getIntForUser(eq(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS), - anyInt(), anyInt())) - .thenReturn(1) + controlsSettingsRepository.setCanShowControlsInLockscreen(true) val component = setupComponent(true) assertEquals(ControlsComponent.Visibility.AVAILABLE, component.getVisibility()) @@ -147,8 +145,7 @@ class ControlsComponentTest : SysuiTestCase() { @Test fun testFeatureEnabledAndCanShowWhileUnlockedVisibility() { - `when`(secureSettings.getInt(eq(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS), anyInt())) - .thenReturn(0) + controlsSettingsRepository.setCanShowControlsInLockscreen(false) `when`(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(STRONG_AUTH_NOT_REQUIRED) `when`(keyguardStateController.isUnlocked()).thenReturn(true) @@ -187,7 +184,7 @@ class ControlsComponentTest : SysuiTestCase() { lockPatternUtils, keyguardStateController, userTracker, - secureSettings, + controlsSettingsRepository, optionalControlsTileResourceConfiguration ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java index 7a2ba95f74a0..06a944e7a6d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java @@ -361,8 +361,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { assertThat(lp.getMarginEnd()).isEqualTo(margin); }); - // The third view should be at the top end corner. No margin should be applied if not - // specified. + // The third view should be at the top end corner. No margin should be applied. verifyChange(thirdViewInfo, true, lp -> { assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue(); assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue(); @@ -442,65 +441,129 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { } /** - * Ensures the root complication applies margin if specified. + * Ensures layout sets correct max width constraint. */ @Test - public void testRootComplicationSpecifiedMargin() { - final int defaultMargin = 5; - final int complicationMargin = 10; + public void testWidthConstraint() { + final int maxWidth = 20; final ComplicationLayoutEngine engine = - new ComplicationLayoutEngine(mLayout, defaultMargin, mTouchSession, 0, 0); + new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0); - final ViewInfo firstViewInfo = new ViewInfo( + final ViewInfo viewStartDirection = new ViewInfo( new ComplicationLayoutParams( 100, 100, ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.POSITION_END, - ComplicationLayoutParams.DIRECTION_DOWN, - 0), + ComplicationLayoutParams.DIRECTION_START, + 0, + 5, + maxWidth), + Complication.CATEGORY_STANDARD, + mLayout); + final ViewInfo viewEndDirection = new ViewInfo( + new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_START, + ComplicationLayoutParams.DIRECTION_END, + 0, + 5, + maxWidth), Complication.CATEGORY_STANDARD, mLayout); - addComplication(engine, firstViewInfo); + addComplication(engine, viewStartDirection); + addComplication(engine, viewEndDirection); - final ViewInfo secondViewInfo = new ViewInfo( + // Verify both horizontal direction views have max width set correctly, and max height is + // not set. + verifyChange(viewStartDirection, false, lp -> { + assertThat(lp.matchConstraintMaxWidth).isEqualTo(maxWidth); + assertThat(lp.matchConstraintMaxHeight).isEqualTo(0); + }); + verifyChange(viewEndDirection, false, lp -> { + assertThat(lp.matchConstraintMaxWidth).isEqualTo(maxWidth); + assertThat(lp.matchConstraintMaxHeight).isEqualTo(0); + }); + } + + /** + * Ensures layout sets correct max height constraint. + */ + @Test + public void testHeightConstraint() { + final int maxHeight = 20; + final ComplicationLayoutEngine engine = + new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0); + + final ViewInfo viewUpDirection = new ViewInfo( + new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_BOTTOM + | ComplicationLayoutParams.POSITION_END, + ComplicationLayoutParams.DIRECTION_UP, + 0, + 5, + maxHeight), + Complication.CATEGORY_STANDARD, + mLayout); + final ViewInfo viewDownDirection = new ViewInfo( new ComplicationLayoutParams( 100, 100, ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.POSITION_END, - ComplicationLayoutParams.DIRECTION_START, - 0), - Complication.CATEGORY_SYSTEM, + ComplicationLayoutParams.DIRECTION_DOWN, + 0, + 5, + maxHeight), + Complication.CATEGORY_STANDARD, mLayout); - addComplication(engine, secondViewInfo); + addComplication(engine, viewUpDirection); + addComplication(engine, viewDownDirection); - firstViewInfo.clearInvocations(); - secondViewInfo.clearInvocations(); + // Verify both vertical direction views have max height set correctly, and max width is + // not set. + verifyChange(viewUpDirection, false, lp -> { + assertThat(lp.matchConstraintMaxHeight).isEqualTo(maxHeight); + assertThat(lp.matchConstraintMaxWidth).isEqualTo(0); + }); + verifyChange(viewDownDirection, false, lp -> { + assertThat(lp.matchConstraintMaxHeight).isEqualTo(maxHeight); + assertThat(lp.matchConstraintMaxWidth).isEqualTo(0); + }); + } - final ViewInfo thirdViewInfo = new ViewInfo( + /** + * Ensures layout does not set any constraint if not specified. + */ + @Test + public void testConstraintNotSetWhenNotSpecified() { + final ComplicationLayoutEngine engine = + new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0); + + final ViewInfo view = new ViewInfo( new ComplicationLayoutParams( 100, 100, ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.POSITION_END, - ComplicationLayoutParams.DIRECTION_START, - 1, - complicationMargin), - Complication.CATEGORY_SYSTEM, + ComplicationLayoutParams.DIRECTION_DOWN, + 0, + 5), + Complication.CATEGORY_STANDARD, mLayout); - addComplication(engine, thirdViewInfo); + addComplication(engine, view); - // The third view is the root view and has specified margin, which should be applied based - // on its direction. - verifyChange(thirdViewInfo, true, lp -> { - assertThat(lp.getMarginStart()).isEqualTo(0); - assertThat(lp.getMarginEnd()).isEqualTo(complicationMargin); - assertThat(lp.topMargin).isEqualTo(0); - assertThat(lp.bottomMargin).isEqualTo(0); + // Verify neither max height nor max width set. + verifyChange(view, false, lp -> { + assertThat(lp.matchConstraintMaxHeight).isEqualTo(0); + assertThat(lp.matchConstraintMaxWidth).isEqualTo(0); }); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java index ce7561e95f1e..fdb4cc4480da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java @@ -97,35 +97,10 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { } /** - * Ensures ComplicationLayoutParams correctly returns whether the complication specified margin. - */ - @Test - public void testIsMarginSpecified() { - final ComplicationLayoutParams paramsNoMargin = new ComplicationLayoutParams( - 100, - 100, - ComplicationLayoutParams.POSITION_TOP - | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_DOWN, - 0); - assertThat(paramsNoMargin.isMarginSpecified()).isFalse(); - - final ComplicationLayoutParams paramsWithMargin = new ComplicationLayoutParams( - 100, - 100, - ComplicationLayoutParams.POSITION_TOP - | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_DOWN, - 0, - 20 /*margin*/); - assertThat(paramsWithMargin.isMarginSpecified()).isTrue(); - } - - /** * Ensures unspecified margin uses default. */ @Test - public void testUnspecifiedMarginUsesDefault() { + public void testDefaultMargin() { final ComplicationLayoutParams params = new ComplicationLayoutParams( 100, 100, @@ -161,13 +136,15 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { ComplicationLayoutParams.POSITION_TOP, ComplicationLayoutParams.DIRECTION_DOWN, 3, - 10); + 10, + 20); final ComplicationLayoutParams copy = new ComplicationLayoutParams(params); assertThat(copy.getDirection() == params.getDirection()).isTrue(); assertThat(copy.getPosition() == params.getPosition()).isTrue(); assertThat(copy.getWeight() == params.getWeight()).isTrue(); assertThat(copy.getMargin(0) == params.getMargin(1)).isTrue(); + assertThat(copy.getConstraint() == params.getConstraint()).isTrue(); assertThat(copy.height == params.height).isTrue(); assertThat(copy.width == params.width).isTrue(); } @@ -193,4 +170,31 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { assertThat(copy.height == params.height).isTrue(); assertThat(copy.width == params.width).isTrue(); } + + /** + * Ensures that constraint is set correctly. + */ + @Test + public void testConstraint() { + final ComplicationLayoutParams paramsWithoutConstraint = new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP, + ComplicationLayoutParams.DIRECTION_DOWN, + 3, + 10); + assertThat(paramsWithoutConstraint.constraintSpecified()).isFalse(); + + final int constraint = 10; + final ComplicationLayoutParams paramsWithConstraint = new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP, + ComplicationLayoutParams.DIRECTION_DOWN, + 3, + 10, + constraint); + assertThat(paramsWithConstraint.constraintSpecified()).isTrue(); + assertThat(paramsWithConstraint.getConstraint()).isEqualTo(constraint); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java index 30ad485d7ac3..e6d3a69593cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java @@ -35,6 +35,7 @@ import android.widget.ImageView; import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.controller.ControlsController; @@ -84,7 +85,10 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { private ArgumentCaptor<ControlsListingController.ControlsListingCallback> mCallbackCaptor; @Mock - private ImageView mView; + private View mView; + + @Mock + private ImageView mHomeControlsView; @Mock private ActivityStarter mActivityStarter; @@ -105,6 +109,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { when(mControlsComponent.getControlsListingController()).thenReturn( Optional.of(mControlsListingController)); when(mControlsComponent.getVisibility()).thenReturn(AVAILABLE); + when(mView.findViewById(R.id.home_controls_chip)).thenReturn(mHomeControlsView); } @Test @@ -206,9 +211,9 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { final ArgumentCaptor<View.OnClickListener> clickListenerCaptor = ArgumentCaptor.forClass(View.OnClickListener.class); - verify(mView).setOnClickListener(clickListenerCaptor.capture()); + verify(mHomeControlsView).setOnClickListener(clickListenerCaptor.capture()); - clickListenerCaptor.getValue().onClick(mView); + clickListenerCaptor.getValue().onClick(mHomeControlsView); verify(mUiEventLogger).log( DreamHomeControlsComplication.DreamHomeControlsChipViewController .DreamOverlayEvent.DREAM_HOME_CONTROLS_TAPPED); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index cedde58746d2..cef452b8ec22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard import android.content.ContentValues import android.content.pm.PackageManager import android.content.pm.ProviderInfo +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SystemUIAppComponentFactoryBase @@ -27,8 +28,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -36,8 +39,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock @@ -74,8 +77,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceProvider() val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock<UserFileManager>().apply { @@ -89,12 +92,22 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, configs = setOf( FakeKeyguardQuickAffordanceConfig( @@ -113,9 +126,10 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest.interactor = KeyguardQuickAffordanceInteractor( 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 623becf166d3..7205f3068abb 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 @@ -37,25 +37,29 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var cameraGestureHelper: CameraGestureHelper @Mock private lateinit var context: Context + private lateinit var underTest: CameraQuickAffordanceConfig @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = CameraQuickAffordanceConfig( + + underTest = + CameraQuickAffordanceConfig( context, - cameraGestureHelper, - ) + ) { + cameraGestureHelper + } } @Test fun `affordance triggered -- camera launch called`() { - //when + // When val result = underTest.onTriggered(null) - //then + // Then verify(cameraGestureHelper) - .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt index cda701819d60..9fa7db127e1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.statusbar.policy.FlashlightController import com.android.systemui.utils.leaks.FakeFlashlightController import com.android.systemui.utils.leaks.LeakCheckedTest +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -38,156 +39,177 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() { @Mock private lateinit var context: Context private lateinit var flashlightController: FakeFlashlightController - private lateinit var underTest : FlashlightQuickAffordanceConfig + private lateinit var underTest: FlashlightQuickAffordanceConfig @Before fun setUp() { injectLeakCheckedDependency(FlashlightController::class.java) MockitoAnnotations.initMocks(this) - flashlightController = SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) as FakeFlashlightController + flashlightController = + SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) + as FakeFlashlightController underTest = FlashlightQuickAffordanceConfig(context, flashlightController) } @Test fun `flashlight is off -- triggered -- icon is on and active`() = runTest { - //given + // given flashlightController.isEnabled = false flashlightController.isAvailable = true val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when underTest.onTriggered(null) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_on, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_on, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight is on -- triggered -- icon is off and inactive`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = true val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when underTest.onTriggered(null) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_off, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_off, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight is on -- receives error -- icon is off and inactive`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightError() val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_off, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_off, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight availability now off -- hidden`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(false) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @Test fun `flashlight availability now on -- flashlight on -- inactive and icon off`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(true) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Active) - assertEquals(R.drawable.ic_flashlight_on, (lastValue.icon as? Icon.Resource)?.res) + assertTrue( + (lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState + is ActivationState.Active + ) + assertEquals(R.drawable.qs_flashlight_icon_on, (lastValue.icon as? Icon.Resource)?.res) job.cancel() } @Test fun `flashlight availability now on -- flashlight off -- inactive and icon off`() = runTest { - //given + // given flashlightController.isEnabled = false flashlightController.isAvailable = false val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(true) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Inactive) - assertEquals(R.drawable.ic_flashlight_off, (lastValue.icon as? Icon.Resource)?.res) + assertTrue( + (lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState + is ActivationState.Inactive + ) + assertEquals(R.drawable.qs_flashlight_icon_off, (lastValue.icon as? Icon.Resource)?.res) job.cancel() } @Test fun `flashlight available -- picker state default`() = runTest { - //given + // given flashlightController.isAvailable = true - //when + // when val result = underTest.getPickerScreenState() - //then + // then assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.Default) } @Test fun `flashlight not available -- picker state unavailable`() = runTest { - //given + // given flashlightController.isAvailable = false - //when + // when val result = underTest.getPickerScreenState() - //then + // then assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt index 8ef921eaa50a..3b0169d77063 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -57,7 +57,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { private lateinit var testScope: TestScope private lateinit var testDispatcher: TestDispatcher - private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager + private lateinit var selectionManager: KeyguardQuickAffordanceLocalUserSelectionManager private lateinit var settings: FakeSettings @Before @@ -75,7 +75,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { testDispatcher = UnconfinedTestDispatcher() testScope = TestScope(testDispatcher) selectionManager = - KeyguardQuickAffordanceSelectionManager( + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock { @@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = FakeUserTracker(), + broadcastDispatcher = fakeBroadcastDispatcher, ) settings = FakeSettings() settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt index d8ee9f113d33..67091a9f40c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Intent import android.content.SharedPreferences import android.content.pm.UserInfo import androidx.test.filters.SmallTest @@ -27,10 +28,15 @@ import com.android.systemui.settings.UserFileManager import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,15 +44,19 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) -class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { +class KeyguardQuickAffordanceLocalUserSelectionManagerTest : SysuiTestCase() { @Mock private lateinit var userFileManager: UserFileManager - private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + private lateinit var underTest: KeyguardQuickAffordanceLocalUserSelectionManager private lateinit var userTracker: FakeUserTracker private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences> @@ -60,15 +70,23 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { sharedPrefs.getOrPut(userId) { FakeSharedPreferences() } } userTracker = FakeUserTracker() + val dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) underTest = - KeyguardQuickAffordanceSelectionManager( + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = userFileManager, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun setSelections() = runTest { overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) @@ -318,6 +336,22 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { job.cancel() } + @Test + fun `responds to backup and restore by reloading the selections from disk`() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + clearInvocations(userFileManager) + + fakeBroadcastDispatcher.registeredReceivers.firstOrNull()?.onReceive(context, Intent()) + + verify(userFileManager, atLeastOnce()).getSharedPreferences(anyString(), anyInt(), anyInt()) + job.cancel() + } + private fun assertSelections( observed: Map<String, List<String>>?, expected: Map<String, List<String>>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt new file mode 100644 index 000000000000..d7e9cf144f88 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt @@ -0,0 +1,219 @@ +/* + * 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.quickaffordance + +import android.content.pm.UserInfo +import android.os.UserHandle +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +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.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceRemoteUserSelectionManagerTest : SysuiTestCase() { + + @Mock private lateinit var userHandle: UserHandle + + private lateinit var underTest: KeyguardQuickAffordanceRemoteUserSelectionManager + + private lateinit var clientFactory: FakeKeyguardQuickAffordanceProviderClientFactory + private lateinit var testScope: TestScope + private lateinit var testDispatcher: TestDispatcher + private lateinit var userTracker: FakeUserTracker + private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient + private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(userHandle.identifier).thenReturn(UserHandle.USER_SYSTEM) + whenever(userHandle.isSystem).thenReturn(true) + client1 = FakeKeyguardQuickAffordanceProviderClient() + client2 = FakeKeyguardQuickAffordanceProviderClient() + + userTracker = FakeUserTracker() + userTracker.set( + userInfos = + listOf( + UserInfo( + UserHandle.USER_SYSTEM, + "Primary", + /* flags= */ 0, + ), + UserInfo( + OTHER_USER_ID_1, + "Secondary 1", + /* flags= */ 0, + ), + UserInfo( + OTHER_USER_ID_2, + "Secondary 2", + /* flags= */ 0, + ), + ), + selectedUserIndex = 0, + ) + + clientFactory = + FakeKeyguardQuickAffordanceProviderClientFactory( + userTracker, + ) { selectedUserId -> + when (selectedUserId) { + OTHER_USER_ID_1 -> client1 + OTHER_USER_ID_2 -> client2 + else -> error("No client set-up for user $selectedUserId!") + } + } + + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + underTest = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = testScope.backgroundScope, + userTracker = userTracker, + clientFactory = clientFactory, + userHandle = userHandle, + ) + } + + @Test + fun `selections - primary user process`() = + testScope.runTest { + val values = mutableListOf<Map<String, List<String>>>() + val job = launch { underTest.selections.toList(values) } + + runCurrent() + assertThat(values.last()).isEmpty() + + client1.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ) + client2.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ) + + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + assertThat(values.last()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ), + ) + ) + + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 2, + ) + runCurrent() + assertThat(values.last()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ), + ) + ) + + job.cancel() + } + + @Test + fun `selections - secondary user process - always empty`() = + testScope.runTest { + whenever(userHandle.isSystem).thenReturn(false) + val values = mutableListOf<Map<String, List<String>>>() + val job = launch { underTest.selections.toList(values) } + + runCurrent() + assertThat(values.last()).isEmpty() + + client1.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ) + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + assertThat(values.last()).isEmpty() + + job.cancel() + } + + @Test + fun setSelections() = + testScope.runTest { + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + + underTest.setSelections( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceIds = listOf(FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1), + ) + runCurrent() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ), + ) + ) + } + + companion object { + private const val OTHER_USER_ID_1 = UserHandle.MIN_SECONDARY_USER_ID + 1 + private const val OTHER_USER_ID_2 = UserHandle.MIN_SECONDARY_USER_ID + 2 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index 5c75417c3473..c40488adf029 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -17,17 +17,23 @@ package com.android.systemui.keyguard.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -39,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,14 +62,24 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { private lateinit var config1: FakeKeyguardQuickAffordanceConfig private lateinit var config2: FakeKeyguardQuickAffordanceConfig + private lateinit var userTracker: FakeUserTracker + private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient + private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient @Before fun setUp() { - config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") - config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + config1 = + FakeKeyguardQuickAffordanceConfig( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1 + ) + config2 = + FakeKeyguardQuickAffordanceConfig( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2 + ) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + userTracker = FakeUserTracker() + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock<UserFileManager>().apply { @@ -75,23 +92,45 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { ) .thenReturn(FakeSharedPreferences()) }, - userTracker = FakeUserTracker(), + userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + client1 = FakeKeyguardQuickAffordanceProviderClient() + client2 = FakeKeyguardQuickAffordanceProviderClient() + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = + FakeKeyguardQuickAffordanceProviderClientFactory( + userTracker, + ) { selectedUserId -> + when (selectedUserId) { + SECONDARY_USER_1 -> client1 + SECONDARY_USER_2 -> client2 + else -> error("No set-up client for user $selectedUserId!") + } + }, + userHandle = UserHandle.SYSTEM, ) underTest = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(config1, config2), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) } @@ -186,7 +225,53 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { ) } - private suspend fun assertSelections( + @Test + fun `selections for secondary user`() = + runBlocking(IMMEDIATE) { + userTracker.set( + userInfos = + listOf( + UserInfo( + UserHandle.USER_SYSTEM, + "Primary", + /* flags= */ 0, + ), + UserInfo( + SECONDARY_USER_1, + "Secondary 1", + /* flags= */ 0, + ), + UserInfo( + SECONDARY_USER_2, + "Secondary 2", + /* flags= */ 0, + ), + ), + selectedUserIndex = 2, + ) + client2.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ) + val observed = mutableListOf<Map<String, List<KeyguardQuickAffordanceConfig>>>() + val job = underTest.selections.onEach { observed.add(it) }.launchIn(this) + yield() + + assertSelections( + observed = observed.last(), + expected = + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + config2, + ), + ) + ) + + job.cancel() + } + + private fun assertSelections( observed: Map<String, List<KeyguardQuickAffordanceConfig>>?, expected: Map<String, List<KeyguardQuickAffordanceConfig>>, ) { @@ -200,5 +285,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private const val SECONDARY_USER_1 = UserHandle.MIN_SECONDARY_USER_ID + 1 + private const val SECONDARY_USER_2 = UserHandle.MIN_SECONDARY_USER_ID + 2 } } 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 c2650ec455d8..1c1f0399bb06 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 @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -29,9 +30,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry @@ -237,8 +240,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { val qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock<UserFileManager>().apply { @@ -252,21 +255,32 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest = KeyguardQuickAffordanceInteractor( 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 b79030602368..11fe905b1d1f 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 @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -26,9 +27,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel @@ -98,8 +101,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock<UserFileManager>().apply { @@ -113,21 +116,32 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) featureFlags = FakeFeatureFlags().apply { 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 8b166bd89426..83a5d0e90c84 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 @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Intent +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -27,9 +28,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor @@ -121,8 +124,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock<UserFileManager>().apply { @@ -136,18 +139,28 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf( @@ -156,6 +169,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { qrCodeScannerAffordanceConfig, ), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest = KeyguardBottomAreaViewModel( diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt index 688c66ac80c9..2c8d7abd4f4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt @@ -46,6 +46,109 @@ class TableLogBufferTest : SysuiTestCase() { } @Test + fun dumpChanges_hasHeader() { + val dumpedString = dumpChanges() + + assertThat(logLines(dumpedString)[0]).isEqualTo(HEADER_PREFIX + NAME) + } + + @Test + fun dumpChanges_hasVersion() { + val dumpedString = dumpChanges() + + assertThat(logLines(dumpedString)[1]).isEqualTo("version $VERSION") + } + + @Test + fun dumpChanges_hasFooter() { + val dumpedString = dumpChanges() + + assertThat(logLines(dumpedString).last()).isEqualTo(FOOTER_PREFIX + NAME) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_str_separatorNotAllowedInPrefix() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("columnName", "stringValue") + } + } + underTest.logDiffs("some${SEPARATOR}thing", TestDiffable(), next) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_bool_separatorNotAllowedInPrefix() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("columnName", true) + } + } + underTest.logDiffs("some${SEPARATOR}thing", TestDiffable(), next) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_int_separatorNotAllowedInPrefix() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("columnName", 567) + } + } + underTest.logDiffs("some${SEPARATOR}thing", TestDiffable(), next) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_str_separatorNotAllowedInColumnName() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("column${SEPARATOR}Name", "stringValue") + } + } + underTest.logDiffs("prefix", TestDiffable(), next) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_bool_separatorNotAllowedInColumnName() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("column${SEPARATOR}Name", true) + } + } + underTest.logDiffs("prefix", TestDiffable(), next) + } + + @Test(expected = IllegalArgumentException::class) + fun dumpChanges_int_separatorNotAllowedInColumnName() { + val next = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("column${SEPARATOR}Name", 456) + } + } + underTest.logDiffs("prefix", TestDiffable(), next) + } + + @Test + fun logChange_bool_dumpsCorrectly() { + systemClock.setCurrentTimeMillis(4000L) + + underTest.logChange("prefix", "columnName", true) + + val dumpedString = dumpChanges() + val expected = + TABLE_LOG_DATE_FORMAT.format(4000L) + + SEPARATOR + + "prefix.columnName" + + SEPARATOR + + "true" + assertThat(dumpedString).contains(expected) + } + + @Test fun dumpChanges_strChange_logsFromNext() { systemClock.setCurrentTimeMillis(100L) @@ -66,11 +169,14 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() - assertThat(dumpedString).contains("prefix") - assertThat(dumpedString).contains("stringValChange") - assertThat(dumpedString).contains("newStringVal") + val expected = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "prefix.stringValChange" + + SEPARATOR + + "newStringVal" + assertThat(dumpedString).contains(expected) assertThat(dumpedString).doesNotContain("prevStringVal") - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L)) } @Test @@ -94,11 +200,14 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() - assertThat(dumpedString).contains("prefix") - assertThat(dumpedString).contains("booleanValChange") - assertThat(dumpedString).contains("true") + val expected = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "prefix.booleanValChange" + + SEPARATOR + + "true" + assertThat(dumpedString).contains(expected) assertThat(dumpedString).doesNotContain("false") - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L)) } @Test @@ -122,11 +231,14 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() - assertThat(dumpedString).contains("prefix") - assertThat(dumpedString).contains("intValChange") - assertThat(dumpedString).contains("67890") + val expected = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "prefix.intValChange" + + SEPARATOR + + "67890" + assertThat(dumpedString).contains(expected) assertThat(dumpedString).doesNotContain("12345") - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L)) } @Test @@ -152,9 +264,9 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() // THEN the dump still works - assertThat(dumpedString).contains("booleanValChange") - assertThat(dumpedString).contains("true") - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L)) + val expected = + TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + "booleanValChange" + SEPARATOR + "true" + assertThat(dumpedString).contains(expected) } @Test @@ -186,15 +298,34 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() - assertThat(dumpedString).contains("valChange") - assertThat(dumpedString).contains("stateValue12") - assertThat(dumpedString).contains("stateValue20") - assertThat(dumpedString).contains("stateValue40") - assertThat(dumpedString).contains("stateValue45") - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(12000L)) - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(20000L)) - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(40000L)) - assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(45000L)) + val expected1 = + TABLE_LOG_DATE_FORMAT.format(12000L) + + SEPARATOR + + "valChange" + + SEPARATOR + + "stateValue12" + val expected2 = + TABLE_LOG_DATE_FORMAT.format(20000L) + + SEPARATOR + + "valChange" + + SEPARATOR + + "stateValue20" + val expected3 = + TABLE_LOG_DATE_FORMAT.format(40000L) + + SEPARATOR + + "valChange" + + SEPARATOR + + "stateValue40" + val expected4 = + TABLE_LOG_DATE_FORMAT.format(45000L) + + SEPARATOR + + "valChange" + + SEPARATOR + + "stateValue45" + assertThat(dumpedString).contains(expected1) + assertThat(dumpedString).contains(expected2) + assertThat(dumpedString).contains(expected3) + assertThat(dumpedString).contains(expected4) } @Test @@ -214,10 +345,73 @@ class TableLogBufferTest : SysuiTestCase() { val dumpedString = dumpChanges() - assertThat(dumpedString).contains("status") - assertThat(dumpedString).contains("in progress") - assertThat(dumpedString).contains("connected") - assertThat(dumpedString).contains("false") + val timestamp = TABLE_LOG_DATE_FORMAT.format(100L) + val expected1 = timestamp + SEPARATOR + "status" + SEPARATOR + "in progress" + val expected2 = timestamp + SEPARATOR + "connected" + SEPARATOR + "false" + assertThat(dumpedString).contains(expected1) + assertThat(dumpedString).contains(expected2) + } + + @Test + fun logChange_rowInitializer_dumpsCorrectly() { + systemClock.setCurrentTimeMillis(100L) + + underTest.logChange("") { row -> + row.logChange("column1", "val1") + row.logChange("column2", 2) + row.logChange("column3", true) + } + + val dumpedString = dumpChanges() + + val timestamp = TABLE_LOG_DATE_FORMAT.format(100L) + val expected1 = timestamp + SEPARATOR + "column1" + SEPARATOR + "val1" + val expected2 = timestamp + SEPARATOR + "column2" + SEPARATOR + "2" + val expected3 = timestamp + SEPARATOR + "column3" + SEPARATOR + "true" + assertThat(dumpedString).contains(expected1) + assertThat(dumpedString).contains(expected2) + assertThat(dumpedString).contains(expected3) + } + + @Test + fun logChangeAndLogDiffs_bothLogged() { + systemClock.setCurrentTimeMillis(100L) + + underTest.logChange("") { row -> + row.logChange("column1", "val1") + row.logChange("column2", 2) + row.logChange("column3", true) + } + + systemClock.setCurrentTimeMillis(200L) + val prevDiffable = object : TestDiffable() {} + val nextDiffable = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("column1", "newVal1") + row.logChange("column2", 222) + row.logChange("column3", false) + } + } + + underTest.logDiffs(columnPrefix = "", prevDiffable, nextDiffable) + + val dumpedString = dumpChanges() + + val timestamp1 = TABLE_LOG_DATE_FORMAT.format(100L) + val expected1 = timestamp1 + SEPARATOR + "column1" + SEPARATOR + "val1" + val expected2 = timestamp1 + SEPARATOR + "column2" + SEPARATOR + "2" + val expected3 = timestamp1 + SEPARATOR + "column3" + SEPARATOR + "true" + val timestamp2 = TABLE_LOG_DATE_FORMAT.format(200L) + val expected4 = timestamp2 + SEPARATOR + "column1" + SEPARATOR + "newVal1" + val expected5 = timestamp2 + SEPARATOR + "column2" + SEPARATOR + "222" + val expected6 = timestamp2 + SEPARATOR + "column3" + SEPARATOR + "false" + assertThat(dumpedString).contains(expected1) + assertThat(dumpedString).contains(expected2) + assertThat(dumpedString).contains(expected3) + assertThat(dumpedString).contains(expected4) + assertThat(dumpedString).contains(expected5) + assertThat(dumpedString).contains(expected6) } @Test @@ -247,14 +441,24 @@ class TableLogBufferTest : SysuiTestCase() { } private fun dumpChanges(): String { - underTest.dumpChanges(PrintWriter(outputWriter)) + underTest.dump(PrintWriter(outputWriter), arrayOf()) return outputWriter.toString() } - private abstract class TestDiffable : Diffable<TestDiffable> { + private fun logLines(string: String): List<String> { + return string.split("\n").filter { it.isNotBlank() } + } + + private open class TestDiffable : Diffable<TestDiffable> { override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {} } } private const val NAME = "TestTableBuffer" private const val MAX_SIZE = 10 + +// Copying these here from [TableLogBuffer] so that we catch any accidental versioning change +private const val HEADER_PREFIX = "SystemUI StateChangeTableSection START: " +private const val FOOTER_PREFIX = "SystemUI StateChangeTableSection END: " +private const val SEPARATOR = "|" // TBD +private const val VERSION = "1" diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt index 84fdfd78e9fc..136ace173795 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT +import com.android.systemui.settings.UserTracker import com.android.systemui.tuner.TunerService import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock @@ -79,6 +80,7 @@ private fun <T> any(): T = Mockito.any<T>() class MediaResumeListenerTest : SysuiTestCase() { @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var device: MediaDeviceData @Mock private lateinit var token: MediaSession.Token @@ -131,12 +133,15 @@ class MediaResumeListenerTest : SysuiTestCase() { whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) whenever(mockContext.packageManager).thenReturn(context.packageManager) whenever(mockContext.contentResolver).thenReturn(context.contentResolver) + whenever(mockContext.userId).thenReturn(context.userId) executor = FakeExecutor(clock) resumeListener = MediaResumeListener( mockContext, broadcastDispatcher, + userTracker, + executor, executor, tunerService, resumeBrowserFactory, @@ -177,6 +182,8 @@ class MediaResumeListenerTest : SysuiTestCase() { MediaResumeListener( context, broadcastDispatcher, + userTracker, + executor, executor, tunerService, resumeBrowserFactory, @@ -185,7 +192,7 @@ class MediaResumeListenerTest : SysuiTestCase() { ) listener.setManager(mediaDataManager) verify(broadcastDispatcher, never()) - .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any()) + .registerReceiver(eq(listener.userUnlockReceiver), any(), any(), any(), anyInt(), any()) // When data is loaded, we do NOT execute or update anything listener.onMediaDataLoaded(KEY, OLD_KEY, data) @@ -289,7 +296,7 @@ class MediaResumeListenerTest : SysuiTestCase() { resumeListener.setManager(mediaDataManager) verify(broadcastDispatcher) .registerReceiver( - eq(resumeListener.userChangeReceiver), + eq(resumeListener.userUnlockReceiver), any(), any(), any(), @@ -299,7 +306,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // When we get an unlock event val intent = Intent(Intent.ACTION_USER_UNLOCKED) - resumeListener.userChangeReceiver.onReceive(context, intent) + intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId) + resumeListener.userUnlockReceiver.onReceive(context, intent) // Then we should attempt to find recent media for each saved component verify(resumeBrowser, times(3)).findRecentMedia() @@ -375,6 +383,8 @@ class MediaResumeListenerTest : SysuiTestCase() { MediaResumeListener( mockContext, broadcastDispatcher, + userTracker, + executor, executor, tunerService, resumeBrowserFactory, @@ -386,7 +396,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // When we load a component that was played recently val intent = Intent(Intent.ACTION_USER_UNLOCKED) - resumeListener.userChangeReceiver.onReceive(mockContext, intent) + intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId) + resumeListener.userUnlockReceiver.onReceive(mockContext, intent) // We add its resume controls verify(resumeBrowser, times(1)).findRecentMedia() @@ -404,6 +415,8 @@ class MediaResumeListenerTest : SysuiTestCase() { MediaResumeListener( mockContext, broadcastDispatcher, + userTracker, + executor, executor, tunerService, resumeBrowserFactory, @@ -415,7 +428,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // When we load a component that is not recent val intent = Intent(Intent.ACTION_USER_UNLOCKED) - resumeListener.userChangeReceiver.onReceive(mockContext, intent) + intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId) + resumeListener.userUnlockReceiver.onReceive(mockContext, intent) // We do not try to add resume controls verify(resumeBrowser, times(0)).findRecentMedia() @@ -443,6 +457,8 @@ class MediaResumeListenerTest : SysuiTestCase() { MediaResumeListener( mockContext, broadcastDispatcher, + userTracker, + executor, executor, tunerService, resumeBrowserFactory, 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 f43a34f6e89b..80adbf025e0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -44,14 +44,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.IntentFilter; import android.content.res.Resources; import android.hardware.display.DisplayManagerGlobal; import android.os.Handler; import android.os.SystemClock; -import android.os.UserHandle; import android.provider.DeviceConfig; import android.telecom.TelecomManager; import android.testing.AndroidTestingRunner; @@ -79,7 +76,6 @@ import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; @@ -119,6 +115,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; +import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -166,7 +163,7 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private Handler mHandler; @Mock - private BroadcastDispatcher mBroadcastDispatcher; + private UserTracker mUserTracker; @Mock private UiEventLogger mUiEventLogger; @Mock @@ -315,14 +312,10 @@ public class NavigationBarTest extends SysuiTestCase { } @Test - public void testRegisteredWithDispatcher() { + public void testRegisteredWithUserTracker() { mNavigationBar.init(); mNavigationBar.onViewAttached(); - verify(mBroadcastDispatcher).registerReceiverWithHandler( - any(BroadcastReceiver.class), - any(IntentFilter.class), - any(Handler.class), - any(UserHandle.class)); + verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class)); } @Test @@ -463,7 +456,7 @@ public class NavigationBarTest extends SysuiTestCase { mStatusBarStateController, mStatusBarKeyguardViewManager, mMockSysUiState, - mBroadcastDispatcher, + mUserTracker, mCommandQueue, Optional.of(mock(Pip.class)), Optional.of(mock(Recents.class)), diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java index c377c374148f..338182a3e304 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java @@ -48,6 +48,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.power.PowerUI.WarningsUI; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -85,6 +86,7 @@ public class PowerUITest extends SysuiTestCase { private PowerUI mPowerUI; @Mock private EnhancedEstimates mEnhancedEstimates; @Mock private PowerManager mPowerManager; + @Mock private UserTracker mUserTracker; @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock private IThermalService mThermalServiceMock; private IThermalEventListener mUsbThermalEventListener; @@ -682,7 +684,8 @@ public class PowerUITest extends SysuiTestCase { private void createPowerUi() { mPowerUI = new PowerUI( mContext, mBroadcastDispatcher, mCommandQueue, mCentralSurfacesOptionalLazy, - mMockWarnings, mEnhancedEstimates, mWakefulnessLifecycle, mPowerManager); + mMockWarnings, mEnhancedEstimates, mWakefulnessLifecycle, mPowerManager, + mUserTracker); mPowerUI.mThermalService = mThermalServiceMock; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 645b1cde632f..23466cc20f44 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestCoroutineScheduler import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +56,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), TestCoroutineScheduler()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 081a2181cfe5..47afa70fa84b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.FooterActionsTestUtils @@ -44,12 +45,9 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,16 +60,20 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) @RunWithLooper class FooterActionsViewModelTest : SysuiTestCase() { + private val testScope = TestScope() private lateinit var utils: FooterActionsTestUtils - private val testDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()) @Before fun setUp() { - utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) + } + + private fun runTest(block: suspend TestScope.() -> Unit) { + testScope.runTest(testBody = block) } @Test - fun settingsButton() = runBlockingTest { + fun settingsButton() = runTest { val underTest = utils.footerActionsViewModel(showPowerButton = false) val settings = underTest.settings @@ -87,7 +89,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun powerButton() = runBlockingTest { + fun powerButton() = runTest { // Without power button. val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false) assertThat(underTestWithoutPower.power).isNull() @@ -114,7 +116,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun userSwitcher() = runBlockingTest { + fun userSwitcher() = runTest { val picture: Drawable = mock() val userInfoController = FakeUserInfoController(FakeInfo(picture = picture)) val settings = FakeSettings() @@ -135,7 +137,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { showPowerButton = false, footerActionsInteractor = utils.footerActionsInteractor( - bgDispatcher = testDispatcher, userSwitcherRepository = utils.userSwitcherRepository( userTracker = userTracker, @@ -143,22 +144,12 @@ class FooterActionsViewModelTest : SysuiTestCase() { userManager = userManager, userInfoController = userInfoController, userSwitcherController = userSwitcherControllerWrapper.controller, - bgDispatcher = testDispatcher, ), ) ) // Collect the user switcher into currentUserSwitcher. - var currentUserSwitcher: FooterActionsButtonViewModel? = null - val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } } - fun currentUserSwitcher(): FooterActionsButtonViewModel? { - // Make sure we finish collecting the current user switcher. This is necessary because - // combined flows launch multiple coroutines in the current scope so we need to make - // sure we process all coroutines triggered by our flow collection before we make - // assertions on the current buttons. - advanceUntilIdle() - return currentUserSwitcher - } + val currentUserSwitcher = collectLastValue(underTest.userSwitcher) // The user switcher is disabled. assertThat(currentUserSwitcher()).isNull() @@ -203,12 +194,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { // in guest mode. userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() } assertThat(iconTint()).isNull() - - job.cancel() } @Test - fun security() = runBlockingTest { + fun security() = runTest { val securityController = FakeSecurityController() val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() @@ -224,22 +213,15 @@ class FooterActionsViewModelTest : SysuiTestCase() { footerActionsInteractor = utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, - bgDispatcher = testDispatcher, securityRepository = utils.securityRepository( securityController = securityController, - bgDispatcher = testDispatcher, ), ), ) // Collect the security model into currentSecurity. - var currentSecurity: FooterActionsSecurityButtonViewModel? = null - val job = launch { underTest.security.collect { currentSecurity = it } } - fun currentSecurity(): FooterActionsSecurityButtonViewModel? { - advanceUntilIdle() - return currentSecurity - } + val currentSecurity = collectLastValue(underTest.security) // By default, we always return a null SecurityButtonConfig. assertThat(currentSecurity()).isNull() @@ -270,12 +252,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { security = currentSecurity() assertThat(security).isNotNull() assertThat(security!!.onClick).isNull() - - job.cancel() } @Test - fun foregroundServices() = runBlockingTest { + fun foregroundServices() = runTest { val securityController = FakeSecurityController() val fgsManagerController = FakeFgsManagerController( @@ -300,21 +280,14 @@ class FooterActionsViewModelTest : SysuiTestCase() { securityRepository = utils.securityRepository( securityController, - bgDispatcher = testDispatcher, ), foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), - bgDispatcher = testDispatcher, ), ) // Collect the security model into currentSecurity. - var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null - val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } } - fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? { - advanceUntilIdle() - return currentForegroundServices - } + val currentForegroundServices = collectLastValue(underTest.foregroundServices) // We don't show the foreground services button if the number of running packages is not // > 1. @@ -356,12 +329,10 @@ class FooterActionsViewModelTest : SysuiTestCase() { } securityController.updateState {} assertThat(currentForegroundServices()?.displayText).isFalse() - - job.cancel() } @Test - fun observeDeviceMonitoringDialogRequests() = runBlockingTest { + fun observeDeviceMonitoringDialogRequests() = runTest { val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() val broadcastDispatcher = mock<BroadcastDispatcher>() @@ -390,7 +361,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, broadcastDispatcher = broadcastDispatcher, - bgDispatcher = testDispatcher, ), ) @@ -415,7 +385,4 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onVisibilityChangeRequested(visible = true) assertThat(underTest.isVisible.value).isTrue() } - - private fun runBlockingTest(block: suspend TestScope.() -> Unit) = - runTest(testDispatcher) { block() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index 013e58ed99d7..69f3e987ec1d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -33,6 +33,9 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.settings.UserContextProvider; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -49,12 +52,16 @@ import org.mockito.MockitoAnnotations; */ public class RecordingControllerTest extends SysuiTestCase { + private FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); @Mock private RecordingController.RecordingStateChangeCallback mCallback; @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private UserContextProvider mUserContextProvider; + @Mock + private UserTracker mUserTracker; private RecordingController mController; @@ -63,7 +70,8 @@ public class RecordingControllerTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new RecordingController(mBroadcastDispatcher, mUserContextProvider); + mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, + mUserContextProvider, mUserTracker); mController.addCallback(mCallback); } @@ -176,9 +184,7 @@ public class RecordingControllerTest extends SysuiTestCase { mController.updateState(true); // and user is changed - Intent intent = new Intent(Intent.ACTION_USER_SWITCHED) - .putExtra(Intent.EXTRA_USER_HANDLE, USER_ID); - mController.mUserChangeReceiver.onReceive(mContext, intent); + mController.mUserChangedCallback.onUserChanged(USER_ID, mContext); // Ensure that the recording was stopped verify(mCallback).onRecordingEnd(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt index 6d9b01e28aa4..020a86611552 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt @@ -50,24 +50,20 @@ class UserFileManagerImplTest : SysuiTestCase() { lateinit var userFileManager: UserFileManagerImpl lateinit var backgroundExecutor: FakeExecutor - @Mock - lateinit var userManager: UserManager - @Mock - lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userManager: UserManager + @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Before fun setUp() { MockitoAnnotations.initMocks(this) backgroundExecutor = FakeExecutor(FakeSystemClock()) - userFileManager = UserFileManagerImpl(context, userManager, - broadcastDispatcher, backgroundExecutor) + userFileManager = + UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor) } @After fun end() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID) dir.deleteRecursively() } @@ -82,13 +78,14 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testGetSharedPreferences() { val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - UserFileManagerImpl.SHARED_PREFS, - TEST_FILE_NAME - ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + UserFileManagerImpl.SHARED_PREFS, + TEST_FILE_NAME + ) assertThat(secondarySharedPref).isNotNull() assertThat(secondaryUserDir.exists()) @@ -101,32 +98,35 @@ class UserFileManagerImplTest : SysuiTestCase() { val userFileManager = spy(userFileManager) userFileManager.start() verify(userFileManager).clearDeletedUserData() - verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java), - any(IntentFilter::class.java), - any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull()) + verify(broadcastDispatcher) + .registerReceiver( + any(BroadcastReceiver::class.java), + any(IntentFilter::class.java), + any(Executor::class.java), + isNull(), + eq(Context.RECEIVER_EXPORTED), + isNull() + ) } @Test fun testClearDeletedUserData() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files" - ) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files") dir.mkdirs() - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + ) file.createNewFile() assertThat(secondaryUserDir.exists()).isTrue() assertThat(file.exists()).isTrue() @@ -139,15 +139,16 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testEnsureParentDirExists() { - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) assertThat(file.parentFile.exists()).isFalse() - userFileManager.ensureParentDirExists(file) + UserFileManagerImpl.ensureParentDirExists(file) assertThat(file.parentFile.exists()).isTrue() } } 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 69a45599668b..b6f74f0a13ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -131,6 +131,7 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -383,7 +384,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mInteractionJankMonitor, mShadeExpansionStateManager), mKeyguardBypassController, mDozeParameters, - mScreenOffAnimationController); + mScreenOffAnimationController, + mock(NotificationWakeUpCoordinatorLogger.class)); mConfigurationController = new ConfigurationControllerImpl(mContext); PulseExpansionHandler expansionHandler = new PulseExpansionHandler( mContext, @@ -499,8 +501,18 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mDumpManager); mNotificationPanelViewController.initDependencies( mCentralSurfaces, + null, () -> {}, mNotificationShelfController); + mNotificationPanelViewController.setTrackingStartedListener(() -> {}); + mNotificationPanelViewController.setOpenCloseListener( + new NotificationPanelViewController.OpenCloseListener() { + @Override + public void onClosingFinished() {} + + @Override + public void onOpenStarted() {} + }); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); @@ -831,7 +843,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { public void handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(true); when(mView.isEnabled()).thenReturn(true); - MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0); + MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0); mNotificationPanelViewController.getStatusBarTouchEventHandler().handleTouchEvent(event); @@ -839,6 +851,17 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() { + when(mCommandQueue.panelsEnabled()).thenReturn(true); + when(mView.isEnabled()).thenReturn(true); + MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0); + + mNotificationPanelViewController.getStatusBarTouchEventHandler().handleTouchEvent(event); + + verify(mView, never()).dispatchTouchEvent(event); + } + + @Test public void testA11y_initializeNode() { AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 42bc7948caf4..8aaa18129834 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -117,13 +117,6 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testCollapsePanels() { - mCommandQueue.animateCollapsePanels(); - waitForIdleSync(); - verify(mCallbacks).animateCollapsePanels(eq(0), eq(false)); - } - - @Test public void testExpandSettings() { String panel = "some_panel"; mCommandQueue.animateExpandSettingsPanel(panel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 15a687d2adc7..452606dfcca4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar; -import static android.content.Intent.ACTION_USER_SWITCHED; - import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -34,7 +32,6 @@ import android.app.Notification; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.os.Handler; @@ -293,11 +290,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { } @Test - public void testActionUserSwitchedCallsOnUserSwitched() { - Intent intent = new Intent() - .setAction(ACTION_USER_SWITCHED) - .putExtra(Intent.EXTRA_USER_HANDLE, mSecondaryUser.id); - mLockscreenUserManager.getBaseBroadcastReceiverForTest().onReceive(mContext, intent); + public void testUserSwitchedCallsOnUserSwitched() { + mLockscreenUserManager.getUserTrackerCallbackForTest().onUserChanged(mSecondaryUser.id, + mContext); verify(mPresenter, times(1)).onUserSwitched(mSecondaryUser.id); } @@ -366,6 +361,10 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { return mBaseBroadcastReceiver; } + public UserTracker.Callback getUserTrackerCallbackForTest() { + return mUserChangedCallback; + } + public ContentObserver getLockscreenSettingsObserverForTest() { return mLockscreenSettingsObserver; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java index 8b7b4dea155f..6bd3f7a27413 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java @@ -26,22 +26,17 @@ import static android.app.NotificationManager.IMPORTANCE_MIN; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.collection.EntryUtilKt.modifyEntry; -import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; @@ -54,10 +49,10 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.CoreStartable; import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -97,7 +92,7 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private HighPriorityProvider mHighPriorityProvider; @Mock private SysuiStatusBarStateController mStatusBarStateController; - @Mock private BroadcastDispatcher mBroadcastDispatcher; + @Mock private UserTracker mUserTracker; private final FakeSettings mFakeSettings = new FakeSettings(); private KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; @@ -117,7 +112,7 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { mKeyguardUpdateMonitor, mHighPriorityProvider, mStatusBarStateController, - mBroadcastDispatcher, + mUserTracker, mFakeSettings, mFakeSettings); mKeyguardNotificationVisibilityProvider = component.getProvider(); @@ -205,23 +200,19 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test - public void notifyListeners_onReceiveUserSwitchBroadcast() { - ArgumentCaptor<BroadcastReceiver> callbackCaptor = - ArgumentCaptor.forClass(BroadcastReceiver.class); - verify(mBroadcastDispatcher).registerReceiver( + public void notifyListeners_onReceiveUserSwitchCallback() { + ArgumentCaptor<UserTracker.Callback> callbackCaptor = + ArgumentCaptor.forClass(UserTracker.Callback.class); + verify(mUserTracker).addCallback( callbackCaptor.capture(), - argThat(intentFilter -> intentFilter.hasAction(Intent.ACTION_USER_SWITCHED)), - isNull(), - isNull(), - eq(Context.RECEIVER_EXPORTED), - isNull()); - BroadcastReceiver callback = callbackCaptor.getValue(); + any()); + UserTracker.Callback callback = callbackCaptor.getValue(); Consumer<String> listener = mock(Consumer.class); mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener); when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD); - callback.onReceive(mContext, new Intent(Intent.ACTION_USER_SWITCHED)); + callback.onUserChanged(CURR_USER_ID, mContext); verify(listener).accept(anyString()); } @@ -619,7 +610,7 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { @BindsInstance KeyguardUpdateMonitor keyguardUpdateMonitor, @BindsInstance HighPriorityProvider highPriorityProvider, @BindsInstance SysuiStatusBarStateController statusBarStateController, - @BindsInstance BroadcastDispatcher broadcastDispatcher, + @BindsInstance UserTracker userTracker, @BindsInstance SecureSettings secureSettings, @BindsInstance GlobalSettings globalSettings ); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index ea311da3e20b..21aae00f12ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.interruption; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.GROUP_ALERT_SUMMARY; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; @@ -33,6 +34,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -390,6 +393,127 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); } + private long makeWhenHoursAgo(long hoursAgo) { + return System.currentTimeMillis() - (1000 * 60 * 60 * hoursAgo); + } + + @Test + public void testShouldHeadsUp_oldWhen_flagDisabled() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(false); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = makeWhenHoursAgo(25); + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any()); + } + + @Test + public void testShouldHeadsUp_oldWhen_whenNow() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any()); + } + + @Test + public void testShouldHeadsUp_oldWhen_whenRecent() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = makeWhenHoursAgo(13); + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any()); + } + + @Test + public void testShouldHeadsUp_oldWhen_whenZero() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = 0L; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(0L), anyLong(), + eq("when <= 0")); + } + + @Test + public void testShouldHeadsUp_oldWhen_whenNegative() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = -1L; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(-1L), anyLong(), + eq("when <= 0")); + } + + @Test + public void testShouldHeadsUp_oldWhen_hasFullScreenIntent() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + long when = makeWhenHoursAgo(25); + + NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silent= */ false); + entry.getSbn().getNotification().when = when; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(when), anyLong(), + eq("full-screen intent")); + } + + @Test + public void testShouldHeadsUp_oldWhen_isForegroundService() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + long when = makeWhenHoursAgo(25); + + NotificationEntry entry = createFgsNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = when; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + + verify(mLogger, never()).logNoHeadsUpOldWhen(any(), anyLong(), anyLong()); + verify(mLogger).logMaybeHeadsUpDespiteOldWhen(eq(entry), eq(when), anyLong(), + eq("foreground service")); + } + + @Test + public void testShouldNotHeadsUp_oldWhen() throws Exception { + ensureStateForHeadsUpWhenAwake(); + when(mFlags.isNoHunForOldWhenEnabled()).thenReturn(true); + long when = makeWhenHoursAgo(25); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.getSbn().getNotification().when = when; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + + verify(mLogger).logNoHeadsUpOldWhen(eq(entry), eq(when), anyLong()); + verify(mLogger, never()).logMaybeHeadsUpDespiteOldWhen(any(), anyLong(), anyLong(), any()); + } + @Test public void testShouldNotFullScreen_notPendingIntent_withStrictFlag() throws Exception { when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true); @@ -763,6 +887,16 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { return createNotification(importance, n); } + private NotificationEntry createFgsNotification(int importance) { + Notification n = new Notification.Builder(getContext(), "a") + .setContentTitle("title") + .setContentText("content text") + .setFlag(FLAG_FOREGROUND_SERVICE, true) + .build(); + + return createNotification(importance, n); + } + private final NotificationInterruptSuppressor mSuppressAwakeHeadsUp = new NotificationInterruptSuppressor() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt new file mode 100644 index 000000000000..2d23f3c4aea8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.statusbar.notification.row + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.systemui.SysuiTestCase +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.PluginManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.SmartReplyController +import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.SmartReplyConstants +import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.SystemClock +import com.android.systemui.wmshell.BubblesManager +import java.util.Optional +import junit.framework.Assert +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ExpandableNotificationRowControllerTest : SysuiTestCase() { + + private val appName = "MyApp" + private val notifKey = "MyNotifKey" + + private val view: ExpandableNotificationRow = mock() + private val activableNotificationViewController: ActivatableNotificationViewController = mock() + private val rivSubComponentFactory: RemoteInputViewSubcomponent.Factory = mock() + private val metricsLogger: MetricsLogger = mock() + private val logBufferLogger: NotificationRowLogger = mock() + private val listContainer: NotificationListContainer = mock() + private val mediaManager: NotificationMediaManager = mock() + private val smartReplyConstants: SmartReplyConstants = mock() + private val smartReplyController: SmartReplyController = mock() + private val pluginManager: PluginManager = mock() + private val systemClock: SystemClock = mock() + private val keyguardBypassController: KeyguardBypassController = mock() + private val groupMembershipManager: GroupMembershipManager = mock() + private val groupExpansionManager: GroupExpansionManager = mock() + private val rowContentBindStage: RowContentBindStage = mock() + private val notifLogger: NotificationLogger = mock() + private val headsUpManager: HeadsUpManager = mock() + private val onExpandClickListener: ExpandableNotificationRow.OnExpandClickListener = mock() + private val statusBarStateController: StatusBarStateController = mock() + private val gutsManager: NotificationGutsManager = mock() + private val onUserInteractionCallback: OnUserInteractionCallback = mock() + private val falsingManager: FalsingManager = mock() + private val falsingCollector: FalsingCollector = mock() + private val featureFlags: FeatureFlags = mock() + private val peopleNotificationIdentifier: PeopleNotificationIdentifier = mock() + private val bubblesManager: BubblesManager = mock() + private val dragController: ExpandableNotificationRowDragController = mock() + private lateinit var controller: ExpandableNotificationRowController + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + controller = + ExpandableNotificationRowController( + view, + activableNotificationViewController, + rivSubComponentFactory, + metricsLogger, + logBufferLogger, + listContainer, + mediaManager, + smartReplyConstants, + smartReplyController, + pluginManager, + systemClock, + appName, + notifKey, + keyguardBypassController, + groupMembershipManager, + groupExpansionManager, + rowContentBindStage, + notifLogger, + headsUpManager, + onExpandClickListener, + statusBarStateController, + gutsManager, + /*allowLongPress=*/ false, + onUserInteractionCallback, + falsingManager, + falsingCollector, + featureFlags, + peopleNotificationIdentifier, + Optional.of(bubblesManager), + dragController + ) + } + + @After + fun tearDown() { + disallowTestableLooperAsMainThread() + } + + @Test + fun offerKeepInParent_parentDismissed() { + whenever(featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_DISMISSAL_ANIMATION)) + .thenReturn(true) + whenever(view.isParentDismissed).thenReturn(true) + + Assert.assertTrue(controller.offerToKeepInParentForAnimation()) + Mockito.verify(view).setKeepInParentForDismissAnimation(true) + } + + @Test + fun offerKeepInParent_parentNotDismissed() { + whenever(featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_DISMISSAL_ANIMATION)) + .thenReturn(true) + + Assert.assertFalse(controller.offerToKeepInParentForAnimation()) + Mockito.verify(view, never()).setKeepInParentForDismissAnimation(anyBoolean()) + } + + @Test + fun removeFromParent_keptForAnimation() { + val parentView: ExpandableNotificationRow = mock() + whenever(view.notificationParent).thenReturn(parentView) + whenever(view.keepInParentForDismissAnimation()).thenReturn(true) + + Assert.assertTrue(controller.removeFromParentIfKeptForAnimation()) + Mockito.verify(parentView).removeChildNotification(view) + } + + @Test + fun removeFromParent_notKeptForAnimation() { + val parentView: ExpandableNotificationRow = mock() + whenever(view.notificationParent).thenReturn(parentView) + + Assert.assertFalse(controller.removeFromParentIfKeptForAnimation()) + Mockito.verifyNoMoreInteractions(parentView) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index ed2afe753a5e..915924f13197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -41,7 +41,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; -import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; 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 5fd9448b129d..fb31bef909e0 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 @@ -301,7 +301,8 @@ public class NotificationTestHelper { public ExpandableNotificationRow createBubble() throws Exception { Notification n = createNotification(false /* isGroupSummary */, - null /* groupKey */, makeBubbleMetadata(null)); + null /* groupKey */, + makeBubbleMetadata(null /* deleteIntent */, false /* autoExpand */)); n.flags |= FLAG_BUBBLE; ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE, mDefaultInflationFlags, IMPORTANCE_HIGH); @@ -334,7 +335,8 @@ public class NotificationTestHelper { public ExpandableNotificationRow createBubbleInGroup() throws Exception { Notification n = createNotification(false /* isGroupSummary */, - GROUP_KEY /* groupKey */, makeBubbleMetadata(null)); + GROUP_KEY /* groupKey */, + makeBubbleMetadata(null /* deleteIntent */, false /* autoExpand */)); n.flags |= FLAG_BUBBLE; ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE, mDefaultInflationFlags, IMPORTANCE_HIGH); @@ -350,7 +352,7 @@ public class NotificationTestHelper { * @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent} */ public NotificationEntry createBubble(@Nullable PendingIntent deleteIntent) { - return createBubble(makeBubbleMetadata(deleteIntent), USER_HANDLE); + return createBubble(makeBubbleMetadata(deleteIntent, false /* autoExpand */), USER_HANDLE); } /** @@ -359,7 +361,16 @@ public class NotificationTestHelper { * @param handle the user to associate with this bubble. */ public NotificationEntry createBubble(UserHandle handle) { - return createBubble(makeBubbleMetadata(null), handle); + return createBubble(makeBubbleMetadata(null /* deleteIntent */, false /* autoExpand */), + handle); + } + + /** + * Returns an {@link NotificationEntry} that should be shown as a auto-expanded bubble. + */ + public NotificationEntry createAutoExpandedBubble() { + return createBubble(makeBubbleMetadata(null /* deleteIntent */, true /* autoExpand */), + USER_HANDLE); } /** @@ -567,7 +578,7 @@ public class NotificationTestHelper { assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); } - private BubbleMetadata makeBubbleMetadata(PendingIntent deleteIntent) { + private BubbleMetadata makeBubbleMetadata(PendingIntent deleteIntent, boolean autoExpand) { Intent target = new Intent(mContext, BubblesTestActivity.class); PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, PendingIntent.FLAG_MUTABLE); @@ -576,6 +587,7 @@ public class NotificationTestHelper { Icon.createWithResource(mContext, R.drawable.android)) .setDeleteIntent(deleteIntent) .setDesiredHeight(314) + .setAutoExpandBubble(autoExpand) .build(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index bf31eb287579..3fccd37d9d7e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -136,7 +136,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { StatusBarManager.DISABLE2_NOTIFICATION_SHADE, false); verify(mCentralSurfaces).updateQsExpansionEnabled(); - verify(mShadeController).animateCollapsePanels(); + verify(mShadeController).animateCollapseShade(); // Trying to open it does nothing. mSbcqCallbacks.animateExpandNotificationsPanel(); @@ -154,7 +154,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE, StatusBarManager.DISABLE2_NONE, false); verify(mCentralSurfaces).updateQsExpansionEnabled(); - verify(mShadeController, never()).animateCollapsePanels(); + verify(mShadeController, never()).animateCollapseShade(); // Can now be opened. mSbcqCallbacks.animateExpandNotificationsPanel(); 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 013e7278753d..ed84e4268c90 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 @@ -392,10 +392,21 @@ public class CentralSurfacesImplTest extends SysuiTestCase { return null; }).when(mNotificationShadeWindowController).batchApplyWindowLayoutParams(any()); - mShadeController = spy(new ShadeControllerImpl(mCommandQueue, - mStatusBarStateController, mNotificationShadeWindowController, - mStatusBarKeyguardViewManager, mContext.getSystemService(WindowManager.class), - () -> Optional.of(mCentralSurfaces), () -> mAssistManager)); + mShadeController = spy(new ShadeControllerImpl( + mCommandQueue, + mKeyguardStateController, + mStatusBarStateController, + mStatusBarKeyguardViewManager, + mStatusBarWindowController, + mNotificationShadeWindowController, + mContext.getSystemService(WindowManager.class), + () -> mAssistManager, + () -> mNotificationGutsManager + )); + mShadeController.setNotificationPanelViewController(mNotificationPanelViewController); + mShadeController.setNotificationShadeWindowViewController( + mNotificationShadeWindowViewController); + mShadeController.setNotificationPresenter(mNotificationPresenter); when(mOperatorNameViewControllerFactory.create(any())) .thenReturn(mOperatorNameViewController); @@ -492,6 +503,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { return mViewRootImpl; } }; + mCentralSurfaces.initShadeVisibilityListener(); when(mViewRootImpl.getOnBackInvokedDispatcher()) .thenReturn(mOnBackInvokedDispatcher); when(mKeyguardViewMediator.registerCentralSurfaces( @@ -807,7 +819,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mNotificationPanelViewController.canPanelBeCollapsed()).thenReturn(true); mOnBackInvokedCallback.getValue().onBackInvoked(); - verify(mShadeController).animateCollapsePanels(); + verify(mShadeController).animateCollapseShade(); } @Test @@ -1030,7 +1042,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test - public void collapseShade_callsAnimateCollapsePanels_whenExpanded() { + public void collapseShade_callsanimateCollapseShade_whenExpanded() { // GIVEN the shade is expanded mCentralSurfaces.onShadeExpansionFullyChanged(true); mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); @@ -1038,12 +1050,12 @@ public class CentralSurfacesImplTest extends SysuiTestCase { // WHEN collapseShade is called mCentralSurfaces.collapseShade(); - // VERIFY that animateCollapsePanels is called - verify(mShadeController).animateCollapsePanels(); + // VERIFY that animateCollapseShade is called + verify(mShadeController).animateCollapseShade(); } @Test - public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() { + public void collapseShade_doesNotCallanimateCollapseShade_whenCollapsed() { // GIVEN the shade is collapsed mCentralSurfaces.onShadeExpansionFullyChanged(false); mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); @@ -1051,12 +1063,12 @@ public class CentralSurfacesImplTest extends SysuiTestCase { // WHEN collapseShade is called mCentralSurfaces.collapseShade(); - // VERIFY that animateCollapsePanels is NOT called - verify(mShadeController, never()).animateCollapsePanels(); + // VERIFY that animateCollapseShade is NOT called + verify(mShadeController, never()).animateCollapseShade(); } @Test - public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() { + public void collapseShadeForBugReport_callsanimateCollapseShade_whenFlagDisabled() { // GIVEN the shade is expanded & flag enabled mCentralSurfaces.onShadeExpansionFullyChanged(true); mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); @@ -1065,12 +1077,12 @@ public class CentralSurfacesImplTest extends SysuiTestCase { // WHEN collapseShadeForBugreport is called mCentralSurfaces.collapseShadeForBugreport(); - // VERIFY that animateCollapsePanels is called - verify(mShadeController).animateCollapsePanels(); + // VERIFY that animateCollapseShade is called + verify(mShadeController).animateCollapseShade(); } @Test - public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() { + public void collapseShadeForBugReport_doesNotCallanimateCollapseShade_whenFlagEnabled() { // GIVEN the shade is expanded & flag enabled mCentralSurfaces.onShadeExpansionFullyChanged(true); mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); @@ -1079,8 +1091,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { // WHEN collapseShadeForBugreport is called mCentralSurfaces.collapseShadeForBugreport(); - // VERIFY that animateCollapsePanels is called - verify(mShadeController, never()).animateCollapsePanels(); + // VERIFY that animateCollapseShade is called + verify(mShadeController, never()).animateCollapseShade(); } @Test 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 d3b541899635..df7ee432e79e 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 @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import android.content.res.ColorStateList; import android.graphics.Color; +import android.hardware.biometrics.BiometricSourceType; import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -398,6 +399,8 @@ public class KeyguardBouncerTest extends SysuiTestCase { @Test public void testShow_delaysIfFaceAuthIsRunning() { + when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)) + .thenReturn(true); when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true); mBouncer.show(true /* reset */); @@ -410,9 +413,10 @@ public class KeyguardBouncerTest extends SysuiTestCase { } @Test - public void testShow_doesNotDelaysIfFaceAuthIsLockedOut() { + public void testShow_doesNotDelaysIfFaceAuthIsNotAllowed() { when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true); - when(mKeyguardUpdateMonitor.isFaceLockedOut()).thenReturn(true); + when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)) + .thenReturn(false); mBouncer.show(true /* reset */); verify(mHandler, never()).postDelayed(any(), anyLong()); 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 de71e2c250c4..e4759057a59c 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 @@ -1442,16 +1442,30 @@ public class ScrimControllerTest extends SysuiTestCase { @Test public void testNotificationTransparency_followsTransitionToFullShade() { + mScrimController.setClipsQsScrim(true); + mScrimController.transitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); finishAnimationsImmediately(); + + assertScrimTinted(Map.of( + mScrimInFront, false, + mScrimBehind, true, + mNotificationsScrim, false + )); + float shadeLockedAlpha = mNotificationsScrim.getViewAlpha(); mScrimController.transitionTo(ScrimState.KEYGUARD); mScrimController.setRawPanelExpansionFraction(1.0f); finishAnimationsImmediately(); float keyguardAlpha = mNotificationsScrim.getViewAlpha(); - mScrimController.setClipsQsScrim(true); + assertScrimTinted(Map.of( + mScrimInFront, true, + mScrimBehind, true, + mNotificationsScrim, true + )); + float progress = 0.5f; float lsNotifProgress = 0.3f; mScrimController.setTransitionToFullShadeProgress(progress, lsNotifProgress); 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 bf5186b6324d..e467d9399059 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 @@ -307,6 +307,17 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() { + when(mKeyguardStateController.isOccluded()).thenReturn(true); + mStatusBarKeyguardViewManager.onPanelExpansionChanged( + expansionEvent( + /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE, + /* expanded= */ true, + /* tracking= */ false)); + verify(mPrimaryBouncer, never()).setExpansion(anyFloat()); + } + + @Test public void onPanelExpansionChanged_neverTranslatesBouncerWhenShowBouncer() { // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index ce54d784520c..cae414a3dc67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -263,7 +263,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { while (!runnables.isEmpty()) runnables.remove(0).run(); // Then - verify(mShadeController, atLeastOnce()).collapsePanel(); + verify(mShadeController, atLeastOnce()).collapseShade(); verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(any(), eq(false) /* animate */, any(), any()); @@ -296,7 +296,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry())); // This is called regardless, and simply short circuits when there is nothing to do. - verify(mShadeController, atLeastOnce()).collapsePanel(); + verify(mShadeController, atLeastOnce()).collapseShade(); verify(mAssistManager).hideAssist(); @@ -329,7 +329,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { // Then verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry())); - verify(mShadeController, atLeastOnce()).collapsePanel(); + verify(mShadeController, atLeastOnce()).collapseShade(); verify(mAssistManager).hideAssist(); @@ -357,7 +357,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { // Then verify(mBubblesManager).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry()); - verify(mShadeController, atLeastOnce()).collapsePanel(); + verify(mShadeController, atLeastOnce()).collapseShade(); verify(mAssistManager).hideAssist(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt index b7a6c0125cfa..d35ce76d7a9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt @@ -22,7 +22,7 @@ import android.os.UserHandle import android.provider.Settings.Global import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -45,7 +45,7 @@ class AirplaneModeRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: AirplaneModeRepositoryImpl - @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var logger: TableLogBuffer private lateinit var bgHandler: Handler private lateinit var scope: CoroutineScope private lateinit var settings: FakeSettings diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt index 76016a121e68..5a6bb301743a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt @@ -38,9 +38,9 @@ import org.mockito.MockitoAnnotations @SmallTest @OptIn(ExperimentalCoroutinesApi::class) @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -class AirplaneModeViewModelTest : SysuiTestCase() { +class AirplaneModeViewModelImplTest : SysuiTestCase() { - private lateinit var underTest: AirplaneModeViewModel + private lateinit var underTest: AirplaneModeViewModelImpl @Mock private lateinit var logger: ConnectivityPipelineLogger private lateinit var airplaneModeRepository: FakeAirplaneModeRepository @@ -57,7 +57,7 @@ class AirplaneModeViewModelTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE) underTest = - AirplaneModeViewModel( + AirplaneModeViewModelImpl( interactor, logger, scope, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 71b8bab87d19..b38497a7bbdd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -35,8 +35,9 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SmallTest -class WifiInteractorTest : SysuiTestCase() { +class WifiInteractorImplTest : SysuiTestCase() { private lateinit var underTest: WifiInteractor @@ -47,7 +48,7 @@ class WifiInteractorTest : SysuiTestCase() { fun setUp() { connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() - underTest = WifiInteractor(connectivityRepository, wifiRepository) + underTest = WifiInteractorImpl(connectivityRepository, wifiRepository) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 37457b308597..5c16e1295b65 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -32,12 +32,14 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel @@ -86,9 +88,9 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) - interactor = WifiInteractor(connectivityRepository, wifiRepository) + interactor = WifiInteractorImpl(connectivityRepository, wifiRepository) scope = CoroutineScope(Dispatchers.Unconfined) - airplaneModeViewModel = AirplaneModeViewModel( + airplaneModeViewModel = AirplaneModeViewModelImpl( AirplaneModeInteractor( airplaneModeRepository, connectivityRepository, 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 a1afcd71e3c3..3001b8162185 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 @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -37,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET import com.google.common.truth.Truth.assertThat @@ -81,10 +83,10 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) - interactor = WifiInteractor(connectivityRepository, wifiRepository) + interactor = WifiInteractorImpl(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) airplaneModeViewModel = - AirplaneModeViewModel( + AirplaneModeViewModelImpl( AirplaneModeInteractor( airplaneModeRepository, connectivityRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 7d2c56098584..6a6b2a801ab0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import com.google.common.truth.Truth.assertThat @@ -73,9 +75,9 @@ class WifiViewModelTest : SysuiTestCase() { connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) - interactor = WifiInteractor(connectivityRepository, wifiRepository) + interactor = WifiInteractorImpl(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) - airplaneModeViewModel = AirplaneModeViewModel( + airplaneModeViewModel = AirplaneModeViewModelImpl( AirplaneModeInteractor( airplaneModeRepository, connectivityRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 2e527be1af89..034c618e55d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -95,6 +95,24 @@ class UserRepositoryImplTest : SysuiTestCase() { } @Test + fun userSwitcherSettings_isUserSwitcherEnabled_notInitialized() = runSelfCancelingTest { + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = false, + expectedUserSwitcherEnabled = + context.resources.getBoolean( + com.android.internal.R.bool.config_showUserSwitcherByDefault + ), + ) + } + + @Test fun refreshUsers() = runSelfCancelingTest { underTest = create(this) val initialExpectedValue = diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt index 7df707789290..6bfc2f1a8b8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt @@ -51,15 +51,11 @@ class PairwiseFlowTest : SysuiTestCase() { ) } - @Test - fun notEnough() = runBlocking { - assertThatFlow(flowOf(1).pairwise()).emitsNothing() - } + @Test fun notEnough() = runBlocking { assertThatFlow(flowOf(1).pairwise()).emitsNothing() } @Test fun withInit() = runBlocking { - assertThatFlow(flowOf(2).pairwise(initialValue = 1)) - .emitsExactly(WithPrev(1, 2)) + assertThatFlow(flowOf(2).pairwise(initialValue = 1)).emitsExactly(WithPrev(1, 2)) } @Test @@ -68,25 +64,78 @@ class PairwiseFlowTest : SysuiTestCase() { } @Test - fun withStateFlow() = runBlocking(Dispatchers.Main.immediate) { - val state = MutableStateFlow(1) - val stop = MutableSharedFlow<Unit>() - - val stoppable = merge(state, stop) - .takeWhile { it is Int } - .filterIsInstance<Int>() + fun withTransform() = runBlocking { + assertThatFlow( + flowOf("val1", "val2", "val3").pairwiseBy { prev: String, next: String -> + "$prev|$next" + } + ) + .emitsExactly("val1|val2", "val2|val3") + } - val job1 = launch { - assertThatFlow(stoppable.pairwise()).emitsExactly(WithPrev(1, 2)) - } - state.value = 2 - val job2 = launch { assertThatFlow(stoppable.pairwise()).emitsNothing() } + @Test + fun withGetInit() = runBlocking { + var initRun = false + assertThatFlow( + flowOf("val1", "val2").pairwiseBy( + getInitialValue = { + initRun = true + "initial" + } + ) { prev: String, next: String -> "$prev|$next" } + ) + .emitsExactly("initial|val1", "val1|val2") + assertThat(initRun).isTrue() + } - stop.emit(Unit) + @Test + fun notEnoughWithGetInit() = runBlocking { + var initRun = false + assertThatFlow( + emptyFlow<String>().pairwiseBy( + getInitialValue = { + initRun = true + "initial" + } + ) { prev: String, next: String -> "$prev|$next" } + ) + .emitsNothing() + // Even though the flow will not emit anything, the initial value function should still get + // run. + assertThat(initRun).isTrue() + } - assertThatJob(job1).isCompleted() - assertThatJob(job2).isCompleted() + @Test + fun getInitNotRunWhenFlowNotCollected() = runBlocking { + var initRun = false + flowOf("val1", "val2").pairwiseBy( + getInitialValue = { + initRun = true + "initial" + } + ) { prev: String, next: String -> "$prev|$next" } + + // Since the flow isn't collected, ensure [initialValueFun] isn't run. + assertThat(initRun).isFalse() } + + @Test + fun withStateFlow() = + runBlocking(Dispatchers.Main.immediate) { + val state = MutableStateFlow(1) + val stop = MutableSharedFlow<Unit>() + + val stoppable = merge(state, stop).takeWhile { it is Int }.filterIsInstance<Int>() + + val job1 = launch { assertThatFlow(stoppable.pairwise()).emitsExactly(WithPrev(1, 2)) } + state.value = 2 + val job2 = launch { assertThatFlow(stoppable.pairwise()).emitsNothing() } + + stop.emit(Unit) + + assertThatJob(job1).isCompleted() + assertThatJob(job2).isCompleted() + } } @SmallTest @@ -94,18 +143,17 @@ class PairwiseFlowTest : SysuiTestCase() { class SetChangesFlowTest : SysuiTestCase() { @Test fun simple() = runBlocking { - assertThatFlow( - flowOf(setOf(1, 2, 3), setOf(2, 3, 4)).setChanges() - ).emitsExactly( - SetChanges( - added = setOf(1, 2, 3), - removed = emptySet(), - ), - SetChanges( - added = setOf(4), - removed = setOf(1), - ), - ) + assertThatFlow(flowOf(setOf(1, 2, 3), setOf(2, 3, 4)).setChanges()) + .emitsExactly( + SetChanges( + added = setOf(1, 2, 3), + removed = emptySet(), + ), + SetChanges( + added = setOf(4), + removed = setOf(1), + ), + ) } @Test @@ -147,14 +195,19 @@ class SetChangesFlowTest : SysuiTestCase() { class SampleFlowTest : SysuiTestCase() { @Test fun simple() = runBlocking { - assertThatFlow(flow { yield(); emit(1) }.sample(flowOf(2)) { a, b -> a to b }) + assertThatFlow( + flow { + yield() + emit(1) + } + .sample(flowOf(2)) { a, b -> a to b } + ) .emitsExactly(1 to 2) } @Test fun otherFlowNoValueYet() = runBlocking { - assertThatFlow(flowOf(1).sample(emptyFlow<Unit>())) - .emitsNothing() + assertThatFlow(flowOf(1).sample(emptyFlow<Unit>())).emitsNothing() } @Test @@ -178,13 +231,14 @@ class SampleFlowTest : SysuiTestCase() { } } -private fun <T> assertThatFlow(flow: Flow<T>) = object { - suspend fun emitsExactly(vararg emissions: T) = - assertThat(flow.toList()).containsExactly(*emissions).inOrder() - suspend fun emitsNothing() = - assertThat(flow.toList()).isEmpty() -} +private fun <T> assertThatFlow(flow: Flow<T>) = + object { + suspend fun emitsExactly(vararg emissions: T) = + assertThat(flow.toList()).containsExactly(*emissions).inOrder() + suspend fun emitsNothing() = assertThat(flow.toList()).isEmpty() + } -private fun assertThatJob(job: Job) = object { - fun isCompleted() = assertThat(job.isCompleted).isTrue() -} +private fun assertThatJob(job: Job) = + object { + fun isCompleted() = assertThat(job.isCompleted).isTrue() + } 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 d42f757e16bd..b9dfc27483b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -1423,13 +1423,43 @@ public class BubblesTest extends SysuiTestCase { assertStackCollapsed(); // Post status bar state change update with the same value mBubbleController.onStatusBarStateChanged(false); - // Stack should remain collapsedb + // Stack should remain collapsed assertStackCollapsed(); // Post status bar state change which should trigger bubble to expand mBubbleController.onStatusBarStateChanged(true); assertStackExpanded(); } + /** + * Test to verify behavior for the following scenario: + * <ol> + * <li>device is locked with keyguard on, status bar shade state updates to + * <code>false</code></li> + * <li>notification entry is marked to be a bubble and it is set to auto-expand</li> + * <li>device unlock starts, status bar shade state receives another update to + * <code>false</code></li> + * <li>device is unlocked and status bar shade state is set to <code>true</code></li> + * <li>bubble should be expanded</li> + * </ol> + */ + @Test + public void testOnStatusBarStateChanged_newAutoExpandedBubbleRemainsExpanded() { + // Set device as locked + mBubbleController.onStatusBarStateChanged(false); + + // Create a auto-expanded bubble + NotificationEntry entry = mNotificationTestHelper.createAutoExpandedBubble(); + mEntryListener.onEntryAdded(entry); + + // When unlocking, we may receive duplicate updates with shade=false, ensure they don't + // clear the expanded state + mBubbleController.onStatusBarStateChanged(false); + mBubbleController.onStatusBarStateChanged(true); + + // After unlocking, stack should be expanded + assertStackExpanded(); + } + @Test public void testSetShouldAutoExpand_notifiesFlagChanged() { mBubbleController.updateBubble(mBubbleEntry); 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 fa3cc9905c3f..bf2235aa98a9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -136,6 +136,8 @@ public abstract class SysuiTestCase { InstrumentationRegistry.getArguments()); if (TestableLooper.get(this) != null) { TestableLooper.get(this).processAllMessages(); + // Must remove static reference to this test object to prevent leak (b/261039202) + TestableLooper.remove(this); } disallowTestableLooperAsMainThread(); mContext.cleanUpReceivers(this.getClass().getSimpleName()); 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 new file mode 100644 index 000000000000..8176dd07b84a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Rect + +class FakeOverlapDetector : OverlapDetector { + var shouldReturn: Boolean = false + + override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + return shouldReturn + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt new file mode 100644 index 000000000000..b7a8d2e9f684 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt @@ -0,0 +1,40 @@ +/* + * 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.coroutines + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent + +/** Collect [flow] in a new [Job] and return a getter for the last collected value. */ +fun <T> TestScope.collectLastValue( + flow: Flow<T>, + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, +): () -> T? { + var lastValue: T? = null + backgroundScope.launch(context, start) { flow.collect { lastValue = it } } + return { + runCurrent() + lastValue + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt new file mode 100644 index 000000000000..d85dd2e7bc8a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient + +class FakeKeyguardQuickAffordanceProviderClientFactory( + private val userTracker: UserTracker, + private val callback: (Int) -> KeyguardQuickAffordanceProviderClient = { + FakeKeyguardQuickAffordanceProviderClient() + }, +) : KeyguardQuickAffordanceProviderClientFactory { + + override fun create(): KeyguardQuickAffordanceProviderClient { + return callback(userTracker.userId) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 63448e236867..1a893f8c523c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -56,7 +56,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler /** * Util class to create real implementations of the FooterActions repositories, viewModel and @@ -65,6 +66,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, + private val scheduler: TestCoroutineScheduler, ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { @@ -105,7 +107,7 @@ class FooterActionsTestUtils( foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), broadcastDispatcher: BroadcastDispatcher = mock(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): FooterActionsInteractor { return FooterActionsInteractorImpl( activityStarter, @@ -126,7 +128,7 @@ class FooterActionsTestUtils( /** Create a [SecurityRepository] to be used in tests. */ fun securityRepository( securityController: SecurityController = FakeSecurityController(), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), ): SecurityRepository { return SecurityRepositoryImpl( securityController, @@ -145,7 +147,7 @@ class FooterActionsTestUtils( fun userSwitcherRepository( @Application context: Context = this.context.applicationContext, bgHandler: Handler = Handler(testableLooper.looper), - bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler), userManager: UserManager = mock(), userTracker: UserTracker = FakeUserTracker(), userSwitcherController: UserSwitcherController = mock(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index a7eadba60ddc..0dd1fc758a27 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -66,7 +66,8 @@ class FakeUserTracker( _userId = _userInfo.id _userHandle = UserHandle.of(_userId) - callbacks.forEach { it.onUserChanged(_userId, userContext) } + val copy = callbacks.toList() + copy.forEach { it.onUserChanged(_userId, userContext) } } fun onProfileChanged() { diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java index 540ed4cdb330..3487613d313c 100644 --- a/services/core/java/com/android/server/DockObserver.java +++ b/services/core/java/com/android/server/DockObserver.java @@ -19,6 +19,7 @@ package com.android.server; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; @@ -73,6 +74,7 @@ final class DockObserver extends SystemService { private final boolean mAllowTheaterModeWakeFromDock; private final List<ExtconStateConfig> mExtconStateConfigs; + private DeviceProvisionedObserver mDeviceProvisionedObserver; static final class ExtconStateProvider { private final Map<String, String> mState; @@ -110,7 +112,7 @@ final class DockObserver extends SystemService { Slog.w(TAG, "No state file found at: " + stateFilePath); return new ExtconStateProvider(new HashMap<>()); } catch (Exception e) { - Slog.e(TAG, "" , e); + Slog.e(TAG, "", e); return new ExtconStateProvider(new HashMap<>()); } } @@ -136,7 +138,7 @@ final class DockObserver extends SystemService { private static List<ExtconStateConfig> loadExtconStateConfigs(Context context) { String[] rows = context.getResources().getStringArray( - com.android.internal.R.array.config_dockExtconStateMapping); + com.android.internal.R.array.config_dockExtconStateMapping); try { ArrayList<ExtconStateConfig> configs = new ArrayList<>(); for (String row : rows) { @@ -167,6 +169,7 @@ final class DockObserver extends SystemService { com.android.internal.R.bool.config_allowTheaterModeWakeFromDock); mKeepDreamingWhenUndocking = context.getResources().getBoolean( com.android.internal.R.bool.config_keepDreamingWhenUndocking); + mDeviceProvisionedObserver = new DeviceProvisionedObserver(mHandler); mExtconStateConfigs = loadExtconStateConfigs(context); @@ -199,15 +202,19 @@ final class DockObserver extends SystemService { if (phase == PHASE_ACTIVITY_MANAGER_READY) { synchronized (mLock) { mSystemReady = true; - - // don't bother broadcasting undocked here - if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { - updateLocked(); - } + mDeviceProvisionedObserver.onSystemReady(); + updateIfDockedLocked(); } } } + private void updateIfDockedLocked() { + // don't bother broadcasting undocked here + if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { + updateLocked(); + } + } + private void setActualDockStateLocked(int newState) { mActualDockState = newState; if (!mUpdatesStopped) { @@ -252,8 +259,7 @@ final class DockObserver extends SystemService { // Skip the dock intent if not yet provisioned. final ContentResolver cr = getContext().getContentResolver(); - if (Settings.Global.getInt(cr, - Settings.Global.DEVICE_PROVISIONED, 0) == 0) { + if (!mDeviceProvisionedObserver.isDeviceProvisioned()) { Slog.i(TAG, "Device not provisioned, skipping dock broadcast"); return; } @@ -419,4 +425,48 @@ final class DockObserver extends SystemService { } } } + + private final class DeviceProvisionedObserver extends ContentObserver { + private boolean mRegistered; + + public DeviceProvisionedObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + synchronized (mLock) { + updateRegistration(); + if (isDeviceProvisioned()) { + // Send the dock broadcast if device is docked after provisioning. + updateIfDockedLocked(); + } + } + } + + void onSystemReady() { + updateRegistration(); + } + + private void updateRegistration() { + boolean register = !isDeviceProvisioned(); + if (register == mRegistered) { + return; + } + final ContentResolver resolver = getContext().getContentResolver(); + if (register) { + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, this); + } else { + resolver.unregisterContentObserver(this); + } + mRegistered = register; + } + + boolean isDeviceProvisioned() { + return Settings.Global.getInt(getContext().getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + } + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 94b67cedf86c..598e2b990ea5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -452,13 +452,6 @@ public class FingerprintService extends SystemService { return -1; } - if (!Utils.isUserEncryptedOrLockdown(mLockPatternUtils, userId)) { - // If this happens, something in KeyguardUpdateMonitor is wrong. This should only - // ever be invoked when the user is encrypted or lockdown. - Slog.e(TAG, "detectFingerprint invoked when user is not encrypted or lockdown"); - return -1; - } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for detectFingerprint"); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 0d031333443e..b1c986e6558a 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -2197,6 +2197,15 @@ public final class PowerManagerService extends SystemService if (sQuiescent) { mDirty |= DIRTY_QUIESCENT; } + PowerGroup defaultGroup = mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP); + if (defaultGroup.getWakefulnessLocked() == WAKEFULNESS_DOZING) { + // Workaround for b/187231320 where the AOD can get stuck in a "half on / + // half off" state when a non-default-group VirtualDisplay causes the global + // wakefulness to change to awake, even though the default display is + // dozing. We set sandman summoned to restart dreaming to get it unstuck. + // TODO(b/255688811) - fix this so that AOD never gets interrupted at all. + defaultGroup.setSandmanSummonedLocked(true); + } break; case WAKEFULNESS_ASLEEP: diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java index 4b8c7c176fda..36293d518f51 100644 --- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java +++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java @@ -107,6 +107,7 @@ public class TrustAgentWrapper { // Trust state private boolean mTrusted; private boolean mWaitingForTrustableDowngrade = false; + private boolean mWithinSecurityLockdownWindow = false; private boolean mTrustable; private CharSequence mMessage; private boolean mDisplayTrustGrantedMessage; @@ -160,6 +161,7 @@ public class TrustAgentWrapper { mDisplayTrustGrantedMessage = (flags & FLAG_GRANT_TRUST_DISPLAY_MESSAGE) != 0; if ((flags & FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0) { mWaitingForTrustableDowngrade = true; + setSecurityWindowTimer(); } else { mWaitingForTrustableDowngrade = false; } @@ -452,6 +454,9 @@ public class TrustAgentWrapper { if (mBound) { scheduleRestart(); } + if (mWithinSecurityLockdownWindow) { + mTrustManagerService.lockUser(mUserId); + } // mTrustDisabledByDpm maintains state } }; @@ -673,6 +678,22 @@ public class TrustAgentWrapper { } } + private void setSecurityWindowTimer() { + mWithinSecurityLockdownWindow = true; + long expiration = SystemClock.elapsedRealtime() + (15 * 1000); // timer for 15 seconds + mAlarmManager.setExact( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + expiration, + TAG, + new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + mWithinSecurityLockdownWindow = false; + } + }, + Handler.getMain()); + } + public boolean isManagingTrust() { return mManagingTrust && !mTrustDisabledByDpm; } @@ -691,7 +712,6 @@ public class TrustAgentWrapper { public void destroy() { mHandler.removeMessages(MSG_RESTART_TIMEOUT); - if (!mBound) { return; } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 66992aa79fc2..cf728330e121 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8920,9 +8920,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN) - && getParent().getConfiguration().orientation == ORIENTATION_PORTRAIT - && getParent().getWindowConfiguration().getWindowingMode() - == WINDOWING_MODE_FULLSCREEN) { + && isParentFullscreenPortrait()) { // We are using the parent configuration here as this is the most recent one that gets // passed to onConfigurationChanged when a relevant change takes place return info.getMinAspectRatio(); @@ -8945,6 +8943,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return info.getMinAspectRatio(); } + private boolean isParentFullscreenPortrait() { + final WindowContainer parent = getParent(); + return parent != null + && parent.getConfiguration().orientation == ORIENTATION_PORTRAIT + && parent.getWindowConfiguration().getWindowingMode() == WINDOWING_MODE_FULLSCREEN; + } + /** * Returns true if the activity has maximum or minimum aspect ratio. */ diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 092848ac94f6..435caa7adc08 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -55,7 +55,9 @@ import static android.content.pm.ActivityInfo.launchModeToString; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.INVALID_UID; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; @@ -2398,6 +2400,11 @@ class ActivityStarter { if (actuallyMoved) { // Only record if the activity actually moved. mMovedToTopActivity = act; + if (mNoAnimation) { + act.mDisplayContent.prepareAppTransition(TRANSIT_NONE); + } else { + act.mDisplayContent.prepareAppTransition(TRANSIT_TO_FRONT); + } } act.updateOptionsLocked(mOptions); deliverNewIntent(act, intentGrants); diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index d3452277a29f..cd26e2eb9c53 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -226,6 +226,9 @@ class BLASTSyncEngine { } private void setReady(boolean ready) { + if (mReady == ready) { + return; + } ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready", mSyncId); mReady = ready; if (!ready) return; @@ -239,7 +242,9 @@ class BLASTSyncEngine { ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Adding to group: %s", mSyncId, wc); wc.setSyncGroup(this); wc.prepareSync(); - mWm.mWindowPlacerLocked.requestTraversal(); + if (mReady) { + mWm.mWindowPlacerLocked.requestTraversal(); + } } void onCancelSync(WindowContainer wc) { diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index b033dca465f4..a32e46078d82 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -341,7 +341,11 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { if (childArea == null) { continue; } - pw.println(prefix + "* " + childArea.getName()); + pw.print(prefix + "* " + childArea.getName()); + if (childArea.isOrganized()) { + pw.print(" (organized)"); + } + pw.println(); if (childArea.isTaskDisplayArea()) { // TaskDisplayArea can only contain task. And it is already printed by display. continue; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 3a936a5d7378..b9aeec63daf0 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3353,7 +3353,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } mWmService.mLatencyTracker.onActionStart(ACTION_ROTATE_SCREEN); - controller.mTransitionMetricsReporter.associate(t, + controller.mTransitionMetricsReporter.associate(t.getToken(), startTime -> mWmService.mLatencyTracker.onActionEnd(ACTION_ROTATE_SCREEN)); startAsyncRotation(false /* shouldDebounce */); } @@ -3458,9 +3458,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Override public void dump(PrintWriter pw, String prefix, boolean dumpAll) { - super.dump(pw, prefix, dumpAll); pw.print(prefix); - pw.println("Display: mDisplayId=" + mDisplayId + " rootTasks=" + getRootTaskCount()); + pw.println("Display: mDisplayId=" + mDisplayId + (isOrganized() ? " (organized)" : "")); final String subPrefix = " " + prefix; pw.print(subPrefix); pw.print("init="); pw.print(mInitialDisplayWidth); pw.print("x"); pw.print(mInitialDisplayHeight); pw.print(" "); pw.print(mInitialDisplayDensity); @@ -3491,6 +3490,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp pw.println(" mTouchExcludeRegion=" + mTouchExcludeRegion); pw.println(); + super.dump(pw, prefix, dumpAll); pw.print(prefix); pw.print("mLayoutSeq="); pw.println(mLayoutSeq); pw.print(" mCurrentFocus="); pw.println(mCurrentFocus); @@ -3582,6 +3582,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp pw.println(); mInsetsStateController.dump(prefix, pw); mDwpcHelper.dump(prefix, pw); + pw.println(); } @Override diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 2866f423dc07..89cad9c53464 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -3457,7 +3457,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> final DisplayContent display = getChildAt(i); display.dump(pw, prefix, dumpAll); } - pw.println(); } /** diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 7cac01faf1a0..cb254982a780 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1165,8 +1165,16 @@ class TaskFragment extends WindowContainer<WindowContainer> { } next.delayedResume = false; - final TaskDisplayArea taskDisplayArea = getDisplayArea(); + // If we are currently pausing an activity, then don't do anything until that is done. + final boolean allPausedComplete = mRootWindowContainer.allPausedActivitiesComplete(); + if (!allPausedComplete) { + ProtoLog.v(WM_DEBUG_STATES, + "resumeTopActivity: Skip resume: some activity pausing."); + return false; + } + + final TaskDisplayArea taskDisplayArea = getDisplayArea(); // If the top activity is the resumed one, nothing to do. if (mResumedActivity == next && next.isState(RESUMED) && taskDisplayArea.allResumedActivitiesComplete()) { @@ -1189,14 +1197,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return false; } - // If we are currently pausing an activity, then don't do anything until that is done. - final boolean allPausedComplete = mRootWindowContainer.allPausedActivitiesComplete(); - if (!allPausedComplete) { - ProtoLog.v(WM_DEBUG_STATES, - "resumeTopActivity: Skip resume: some activity pausing."); - return false; - } - // If we are sleeping, and there is no resumed activity, and the top activity is paused, // well that is the state we want. if (mLastPausedActivity == next && shouldSleepOrShutDownActivities()) { @@ -2605,6 +2605,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { return false; } + @Override + boolean canCustomizeAppTransition() { + // This is only called when the app transition is going to be played by system server. In + // this case, we should allow custom app transition for fullscreen embedded TaskFragment + // just like Activity. + return isEmbedded() && matchParentBounds(); + } + /** Clear {@link #mLastPausedActivity} for all {@link TaskFragment} children */ void clearLastPausedActivity() { forAllTaskFragments(taskFragment -> taskFragment.mLastPausedActivity = null); diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java index 39499523833e..14a2d0383019 100644 --- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java @@ -277,31 +277,29 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { // is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is // different, we should recalcuating the bounds. boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = false; - // shouldSetAsOverrideWindowingMode is set if the task needs to retain the launchMode - // regardless of the windowing mode of the parent. - boolean shouldSetAsOverrideWindowingMode = false; - if (launchMode == WINDOWING_MODE_PINNED) { - if (DEBUG) appendLog("picture-in-picture"); - } else if (!root.isResizeable()) { - if (shouldLaunchUnresizableAppInFreeformInFreeformMode(root, suggestedDisplayArea, - options)) { - launchMode = WINDOWING_MODE_UNDEFINED; - if (outParams.mBounds.isEmpty()) { - getTaskBounds(root, suggestedDisplayArea, layout, launchMode, hasInitialBounds, - outParams.mBounds); - hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true; + if (suggestedDisplayArea.inFreeformWindowingMode()) { + if (launchMode == WINDOWING_MODE_PINNED) { + if (DEBUG) appendLog("picture-in-picture"); + } else if (!root.isResizeable()) { + if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) { + launchMode = WINDOWING_MODE_FREEFORM; + if (outParams.mBounds.isEmpty()) { + getTaskBounds(root, suggestedDisplayArea, layout, launchMode, + hasInitialBounds, outParams.mBounds); + hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true; + } + if (DEBUG) appendLog("unresizable-freeform"); + } else { + launchMode = WINDOWING_MODE_FULLSCREEN; + outParams.mBounds.setEmpty(); + if (DEBUG) appendLog("unresizable-forced-maximize"); } - if (DEBUG) appendLog("unresizable-freeform"); - } else { - launchMode = WINDOWING_MODE_FULLSCREEN; - outParams.mBounds.setEmpty(); - shouldSetAsOverrideWindowingMode = true; - if (DEBUG) appendLog("unresizable-forced-maximize"); } + } else { + if (DEBUG) appendLog("non-freeform-task-display-area"); } // If launch mode matches display windowing mode, let it inherit from display. outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode() - && !shouldSetAsOverrideWindowingMode ? WINDOWING_MODE_UNDEFINED : launchMode; if (phase == PHASE_WINDOWING_MODE) { @@ -652,7 +650,7 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { inOutBounds.offset(xOffset, yOffset); } - private boolean shouldLaunchUnresizableAppInFreeformInFreeformMode(ActivityRecord activity, + private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity, TaskDisplayArea displayArea, @Nullable ActivityOptions options) { if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) { // Do not launch the activity in freeform if it explicitly requested fullscreen mode. @@ -665,7 +663,8 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { final int displayOrientation = orientationFromBounds(displayArea.getBounds()); final int activityOrientation = resolveOrientation(activity, displayArea, displayArea.getBounds()); - if (displayOrientation != activityOrientation) { + if (displayArea.getWindowingMode() == WINDOWING_MODE_FREEFORM + && displayOrientation != activityOrientation) { return true; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index b6e52aaff035..ec3962c282f6 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -98,6 +98,7 @@ import com.android.server.wm.utils.RotationAnimationUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -107,7 +108,7 @@ import java.util.function.Predicate; * Represents a logical transition. * @see TransitionController */ -class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListener { +class Transition implements BLASTSyncEngine.TransactionReadyListener { private static final String TAG = "Transition"; private static final String TRACE_NAME_PLAY_TRANSITION = "PlayTransition"; @@ -158,6 +159,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe private @TransitionFlags int mFlags; private final TransitionController mController; private final BLASTSyncEngine mSyncEngine; + private final Token mToken; private RemoteTransition mRemoteTransition = null; /** Only use for clean-up after binder death! */ @@ -165,9 +167,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe private SurfaceControl.Transaction mFinishTransaction = null; /** - * Contains change infos for both participants and all ancestors. We have to track ancestors - * because they are all promotion candidates and thus we need their start-states - * to be captured. + * Contains change infos for both participants and all remote-animatable ancestors. The + * ancestors can be the promotion candidates so their start-states need to be captured. + * @see #getAnimatableParent */ final ArrayMap<WindowContainer, ChangeInfo> mChanges = new ArrayMap<>(); @@ -220,10 +222,27 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mFlags = flags; mController = controller; mSyncEngine = syncEngine; + mToken = new Token(this); controller.mTransitionTracer.logState(this); } + @Nullable + static Transition fromBinder(@Nullable IBinder token) { + if (token == null) return null; + try { + return ((Token) token).mTransition.get(); + } catch (ClassCastException e) { + Slog.w(TAG, "Invalid transition token: " + token, e); + return null; + } + } + + @NonNull + IBinder getToken() { + return mToken; + } + void addFlag(int flag) { mFlags |= flag; } @@ -399,8 +418,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mSyncId, wc); // "snapshot" all parents (as potential promotion targets). Do this before checking // if this is already a participant in case it has since been re-parented. - for (WindowContainer curr = wc.getParent(); curr != null && !mChanges.containsKey(curr); - curr = curr.getParent()) { + for (WindowContainer<?> curr = getAnimatableParent(wc); + curr != null && !mChanges.containsKey(curr); + curr = getAnimatableParent(curr)) { mChanges.put(curr, new ChangeInfo(curr)); if (isReadyGroup(curr)) { mReadyTracker.addGroup(curr); @@ -733,6 +753,11 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION, System.identityHashCode(this)); } + // Close the transactions now. They were originally copied to Shell in case we needed to + // apply them due to a remote failure. Since we don't need to apply them anymore, free them + // immediately. + if (mStartTransaction != null) mStartTransaction.close(); + if (mFinishTransaction != null) mFinishTransaction.close(); mStartTransaction = mFinishTransaction = null; if (mState < STATE_PLAYING) { throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId); @@ -874,6 +899,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mController.mAtm.mWindowManager.updateRotation(false /* alwaysSendConfiguration */, false /* forceRelayout */); } + cleanUpInternal(); } void abort() { @@ -916,15 +942,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe dc.getPendingTransaction().merge(transaction); mSyncId = -1; mOverrideOptions = null; + cleanUpInternal(); return; } - // Ensure that wallpaper visibility is updated with the latest wallpaper target. - for (int i = mParticipants.size() - 1; i >= 0; --i) { - final WindowContainer<?> wc = mParticipants.valueAt(i); - if (isWallpaper(wc) && wc.getDisplayContent() != null) { - wc.getDisplayContent().mWallpaperController.adjustWallpaperWindows(); - } - } mState = STATE_PLAYING; mStartTransaction = transaction; @@ -1034,7 +1054,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Calling onTransitionReady: %s", info); mController.getTransitionPlayer().onTransitionReady( - this, info, transaction, mFinishTransaction); + mToken, info, transaction, mFinishTransaction); + // Since we created root-leash but no longer reference it from core, release it now + info.releaseAnimSurfaces(); if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION, System.identityHashCode(this)); @@ -1067,7 +1089,17 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe if (mFinishTransaction != null) { mFinishTransaction.apply(); } - mController.finishTransition(this); + mController.finishTransition(mToken); + } + + private void cleanUpInternal() { + // Clean-up any native references. + for (int i = 0; i < mChanges.size(); ++i) { + final ChangeInfo ci = mChanges.valueAt(i); + if (ci.mSnapshot != null) { + ci.mSnapshot.release(); + } + } } /** @see RecentsAnimationController#attachNavigationBarToApp */ @@ -1269,6 +1301,16 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return sb.toString(); } + /** Returns the parent that the remote animator can animate or control. */ + private static WindowContainer<?> getAnimatableParent(WindowContainer<?> wc) { + WindowContainer<?> parent = wc.getParent(); + while (parent != null + && (!parent.canCreateRemoteAnimationTarget() && !parent.isOrganized())) { + parent = parent.getParent(); + } + return parent; + } + private static boolean reportIfNotTop(WindowContainer wc) { // Organized tasks need to be reported anyways because Core won't show() their surfaces // and we can't rely on onTaskAppeared because it isn't in sync. @@ -1492,7 +1534,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe intermediates.clear(); boolean foundParentInTargets = false; // Collect the intermediate parents between target and top changed parent. - for (WindowContainer<?> p = wc.getParent(); p != null; p = p.getParent()) { + for (WindowContainer<?> p = getAnimatableParent(wc); p != null; + p = getAnimatableParent(p)) { final ChangeInfo parentChange = changes.get(p); if (parentChange == null || !parentChange.hasChanged(p)) break; if (p.mRemoteToken == null) { @@ -1850,10 +1893,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return isCollecting() && mSyncId >= 0; } - static Transition fromBinder(IBinder binder) { - return (Transition) binder; - } - @VisibleForTesting static class ChangeInfo { private static final int FLAG_NONE = 0; @@ -2345,4 +2384,18 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } } } + + private static class Token extends Binder { + final WeakReference<Transition> mTransition; + + Token(Transition transition) { + mTransition = new WeakReference<>(transition); + } + + @Override + public String toString() { + return "Token{" + Integer.toHexString(System.identityHashCode(this)) + " " + + mTransition.get() + "}"; + } + } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index e4d39b94ab10..d3d1c163aa19 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -458,8 +458,9 @@ class TransitionController { info = new ActivityManager.RunningTaskInfo(); startTask.fillTaskInfo(info); } - mTransitionPlayer.requestStartTransition(transition, new TransitionRequestInfo( - transition.mType, info, remoteTransition, displayChange)); + mTransitionPlayer.requestStartTransition(transition.getToken(), + new TransitionRequestInfo(transition.mType, info, remoteTransition, + displayChange)); transition.setRemoteTransition(remoteTransition); } catch (RemoteException e) { Slog.e(TAG, "Error requesting transition", e); diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 908fdbdc74e6..920b1bad48d1 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -113,12 +113,6 @@ class WallpaperController { private boolean mShouldUpdateZoom; - /** - * Temporary storage for taking a screenshot of the wallpaper. - * @see #screenshotWallpaperLocked() - */ - private WindowState mTmpTopWallpaper; - @Nullable private Point mLargestDisplaySize = null; private final FindWallpaperTargetResult mFindResults = new FindWallpaperTargetResult(); @@ -962,21 +956,16 @@ class WallpaperController { } WindowState getTopVisibleWallpaper() { - mTmpTopWallpaper = null; - for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); - token.forAllWindows(w -> { - final WindowStateAnimator winAnim = w.mWinAnimator; - if (winAnim != null && winAnim.getShown() && winAnim.mLastAlpha > 0f) { - mTmpTopWallpaper = w; - return true; + for (int i = token.getChildCount() - 1; i >= 0; i--) { + final WindowState w = token.getChildAt(i); + if (w.mWinAnimator.getShown() && w.mWinAnimator.mLastAlpha > 0f) { + return w; } - return false; - }, true /* traverseTopToBottom */); + } } - - return mTmpTopWallpaper; + return null; } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 63344a0e8678..68b853dd2695 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8878,14 +8878,14 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public List<DisplayInfo> getPossibleDisplayInfo(int displayId, String packageName) { + public List<DisplayInfo> getPossibleDisplayInfo(int displayId) { final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - if (packageName == null || !isRecentsComponent(packageName, callingUid)) { - Slog.e(TAG, "Unable to verify uid for package " + packageName - + " for getPossibleMaximumWindowMetrics"); + if (!mAtmService.isCallerRecents(callingUid)) { + Slog.e(TAG, "Unable to verify uid for getPossibleDisplayInfo" + + " on uid " + callingUid); return new ArrayList<>(); } @@ -8903,31 +8903,6 @@ public class WindowManagerService extends IWindowManager.Stub return mPossibleDisplayInfoMapper.getPossibleDisplayInfos(displayId); } - /** - * Returns {@code true} when the calling package is the recents component. - */ - boolean isRecentsComponent(@NonNull String callingPackageName, int callingUid) { - String recentsPackage; - try { - String recentsComponent = mContext.getResources().getString( - R.string.config_recentsComponentName); - if (recentsComponent == null) { - return false; - } - recentsPackage = ComponentName.unflattenFromString(recentsComponent).getPackageName(); - } catch (Resources.NotFoundException e) { - Slog.e(TAG, "Unable to verify if recents component", e); - return false; - } - try { - return callingUid == mContext.getPackageManager().getPackageUid(callingPackageName, 0) - && callingPackageName.equals(recentsPackage); - } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Unable to verify if recents component", e); - return false; - } - } - void grantEmbeddedWindowFocus(Session session, IBinder focusToken, boolean grantFocus) { synchronized (mGlobalLock) { final EmbeddedWindowController.EmbeddedWindow embeddedWindow = diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index b30fd0729525..d85bd830cc11 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -307,7 +307,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub nextTransition.setAllReady(); } }); - return nextTransition; + return nextTransition.getToken(); } transition = mTransitionController.createTransition(type); } @@ -316,7 +316,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (needsSetReady) { transition.setAllReady(); } - return transition; + return transition.getToken(); } } finally { Binder.restoreCallingIdentity(ident); diff --git a/services/tests/servicestests/src/com/android/server/DockObserverTest.java b/services/tests/servicestests/src/com/android/server/DockObserverTest.java index c325778a5683..ee09074f7625 100644 --- a/services/tests/servicestests/src/com/android/server/DockObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/DockObserverTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Intent; import android.os.Looper; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -74,6 +75,11 @@ public class DockObserverTest { .isEqualTo(Intent.EXTRA_DOCK_STATE_UNDOCKED); } + void setDeviceProvisioned(boolean provisioned) { + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, + provisioned ? 1 : 0); + } + @Before public void setUp() { if (Looper.myLooper() == null) { @@ -131,4 +137,25 @@ public class DockObserverTest { assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY5=5", Intent.EXTRA_DOCK_STATE_HE_DESK); } + + @Test + public void testDockIntentBroadcast_deviceNotProvisioned() + throws ExecutionException, InterruptedException { + DockObserver observer = new DockObserver(mInterceptingContext); + // Set the device as not provisioned. + setDeviceProvisioned(false); + observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + + BroadcastInterceptingContext.FutureIntent futureIntent = + updateExtconDockState(observer, "DOCK=1"); + TestableLooper.get(this).processAllMessages(); + // Verify no broadcast was sent as device was not provisioned. + futureIntent.assertNotReceived(); + + // Ensure we send the broadcast when the device is provisioned. + setDeviceProvisioned(true); + TestableLooper.get(this).processAllMessages(); + assertThat(futureIntent.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1)) + .isEqualTo(Intent.EXTRA_DOCK_STATE_DESK); + } } diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index 6d2631ac3ad4..f28986629c1c 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -456,9 +456,8 @@ public final class DeviceStateProviderImplTest { new DeviceState(1, "CLOSED", 0 /* flags */), new DeviceState(2, "HALF_OPENED", 0 /* flags */) }, mDeviceStateArrayCaptor.getValue()); - // onStateChanged() should be called because the provider could not find the sensor. - verify(listener).onStateChanged(mIntegerCaptor.capture()); - assertEquals(1, mIntegerCaptor.getValue().intValue()); + // onStateChanged() should not be called because the provider could not find the sensor. + verify(listener, never()).onStateChanged(mIntegerCaptor.capture()); } private static Sensor newSensor(String name, String type) throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index a42d009c417c..6325008dc1e0 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -1929,6 +1929,50 @@ public class PowerManagerServiceTest { } @Test + public void testMultiDisplay_defaultDozing_addNewDisplayDefaultGoesBackToDoze() { + final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; + final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1; + final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener = + new AtomicReference<>(); + doAnswer((Answer<Void>) invocation -> { + listener.set(invocation.getArgument(0)); + return null; + }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any()); + final DisplayInfo info = new DisplayInfo(); + info.displayGroupId = nonDefaultDisplayGroupId; + when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info); + + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + createService(); + startSystem(); + + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_AWAKE); + + forceDozing(); + advanceTime(500); + + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_DOZING); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING); + verify(mDreamManagerInternalMock).startDream(eq(true), anyString()); + + listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId); + advanceTime(500); + + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo( + WAKEFULNESS_AWAKE); + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_DOZING); + verify(mDreamManagerInternalMock, times(2)).startDream(eq(true), anyString()); + } + + @Test public void testLastSleepTime_notUpdatedWhenDreaming() { createService(); startSystem(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index d3aa073c84d8..df7b3cdebe28 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -74,15 +74,15 @@ public class SyncEngineTests extends WindowTestsBase { int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); - // Make sure a traversal is requested - verify(mWm.mWindowPlacerLocked, times(1)).requestTraversal(); + // The traversal is not requested because ready is not set. + verify(mWm.mWindowPlacerLocked, times(0)).requestTraversal(); bse.onSurfacePlacement(); verify(listener, times(0)).onTransactionReady(anyInt(), any()); bse.setReady(id); // Make sure a traversal is requested - verify(mWm.mWindowPlacerLocked, times(2)).requestTraversal(); + verify(mWm.mWindowPlacerLocked).requestTraversal(); bse.onSurfacePlacement(); verify(listener, times(1)).onTransactionReady(eq(id), notNull()); @@ -103,14 +103,14 @@ public class SyncEngineTests extends WindowTestsBase { int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); bse.setReady(id); - // Make sure traversals requested (one for add and another for setReady) - verify(mWm.mWindowPlacerLocked, times(2)).requestTraversal(); + // Make sure traversals requested. + verify(mWm.mWindowPlacerLocked).requestTraversal(); bse.onSurfacePlacement(); verify(listener, times(0)).onTransactionReady(anyInt(), any()); mockWC.onSyncFinishedDrawing(); - // Make sure a (third) traversal is requested. - verify(mWm.mWindowPlacerLocked, times(3)).requestTraversal(); + // Make sure the second traversal is requested. + verify(mWm.mWindowPlacerLocked, times(2)).requestTraversal(); bse.onSurfacePlacement(); verify(listener, times(1)).onTransactionReady(eq(id), notNull()); } @@ -127,8 +127,8 @@ public class SyncEngineTests extends WindowTestsBase { int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); bse.setReady(id); - // Make sure traversals requested (one for add and another for setReady) - verify(mWm.mWindowPlacerLocked, times(2)).requestTraversal(); + // Make sure traversals requested. + verify(mWm.mWindowPlacerLocked).requestTraversal(); bse.onSurfacePlacement(); verify(listener, times(0)).onTransactionReady(anyInt(), any()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java index 2a88eb0e7b61..1188f49ae60e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java @@ -825,11 +825,10 @@ public class TaskLaunchParamsModifierTests extends WindowTestsBase { } @Test - public void testLaunchesPortraitUnresizableOnFreeformLandscapeDisplay() { + public void testLaunchesPortraitUnresizableOnFreeformDisplayWithFreeformSizeCompat() { mAtm.mDevEnableNonResizableMultiWindow = true; final TestDisplayContent freeformDisplay = createNewDisplayContent( WINDOWING_MODE_FREEFORM); - assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height()); final ActivityOptions options = ActivityOptions.makeBasic(); mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea(); mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; @@ -837,42 +836,12 @@ public class TaskLaunchParamsModifierTests extends WindowTestsBase { assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setOptions(options).calculate()); - assertEquals(WINDOWING_MODE_UNDEFINED, mResult.mWindowingMode); - } - - @Test - public void testLaunchesLandscapeUnresizableOnFreeformLandscapeDisplay() { - mAtm.mDevEnableNonResizableMultiWindow = true; - final TestDisplayContent freeformDisplay = createNewDisplayContent( - WINDOWING_MODE_FREEFORM); - assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height()); - final ActivityOptions options = ActivityOptions.makeBasic(); - mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea(); - mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; - mActivity.info.screenOrientation = SCREEN_ORIENTATION_LANDSCAPE; - assertEquals(RESULT_CONTINUE, - new CalculateRequestBuilder().setOptions(options).calculate()); - - assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode); - } - - @Test - public void testLaunchesUndefinedUnresizableOnFreeformLandscapeDisplay() { - mAtm.mDevEnableNonResizableMultiWindow = true; - final TestDisplayContent freeformDisplay = createNewDisplayContent( + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, WINDOWING_MODE_FREEFORM); - assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height()); - final ActivityOptions options = ActivityOptions.makeBasic(); - mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea(); - mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; - assertEquals(RESULT_CONTINUE, - new CalculateRequestBuilder().setOptions(options).calculate()); - - assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode); } @Test - public void testForceMaximizingAppsOnNonFreeformDisplay() { + public void testSkipsForceMaximizingAppsOnNonFreeformDisplay() { final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); options.setLaunchBounds(new Rect(0, 0, 200, 100)); @@ -886,9 +855,8 @@ public class TaskLaunchParamsModifierTests extends WindowTestsBase { assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setOptions(options).calculate()); - // Non-resizable apps must be launched in fullscreen in a fullscreen display regardless of - // other properties. - assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode); + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 54bcbd9adf5a..999523f3ea19 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -476,6 +476,8 @@ public class TransitionTests extends WindowTestsBase { wallpaperWindow.mHasSurface = true; doReturn(true).when(mDisplayContent).isAttached(); transition.collect(mDisplayContent); + assertFalse("The change of non-interesting window container should be skipped", + transition.mChanges.containsKey(mDisplayContent.getParent())); mDisplayContent.getWindowConfiguration().setRotation( (mDisplayContent.getWindowConfiguration().getRotation() + 1) % 4); 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 63335086859d..aab70b5f9004 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -369,18 +369,9 @@ public class WallpaperControllerTests extends WindowTestsBase { final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); token.finishSync(t, false /* cancel */); transit.onTransactionReady(transit.getSyncId(), t); - dc.mTransitionController.finishTransition(transit); + dc.mTransitionController.finishTransition(transit.getToken()); assertFalse(wallpaperWindow.isVisible()); assertFalse(token.isVisible()); - - // Assume wallpaper was visible. When transaction is ready without wallpaper target, - // wallpaper should be requested to be invisible. - token.setVisibility(true); - transit = dc.mTransitionController.createTransition(TRANSIT_CLOSE); - dc.mTransitionController.collect(token); - transit.onTransactionReady(transit.getSyncId(), t); - assertFalse(token.isVisibleRequested()); - assertTrue(token.isVisible()); } private static void prepareSmallerSecondDisplay(DisplayContent dc, int width, int height) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index b99fd1606f55..894ba3e95261 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1731,7 +1731,7 @@ class WindowTestsBase extends SystemServiceTestsBase { } void startTransition() { - mOrganizer.startTransition(mLastTransit, null); + mOrganizer.startTransition(mLastTransit.getToken(), null); } void onTransactionReady(SurfaceControl.Transaction t) { @@ -1744,7 +1744,7 @@ class WindowTestsBase extends SystemServiceTestsBase { } public void finish() { - mController.finishTransition(mLastTransit); + mController.finishTransition(mLastTransit.getToken()); } } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 7f8e4ba405d3..741721d14ac1 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -2000,6 +2000,17 @@ public class CarrierConfigManager { "nr_advanced_threshold_bandwidth_khz_int"; /** + * Indicating whether to include LTE cell bandwidths when determining whether the aggregated + * cell bandwidth meets the required threshold for NR advanced. + * + * @see TelephonyDisplayInfo#OVERRIDE_NETWORK_TYPE_NR_ADVANCED + * + * @hide + */ + public static final String KEY_INCLUDE_LTE_FOR_NR_ADVANCED_THRESHOLD_BANDWIDTH_BOOL = + "include_lte_for_nr_advanced_threshold_bandwidth_bool"; + + /** * Boolean indicating if operator name should be shown in the status bar * @hide */ @@ -9001,6 +9012,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL, true); sDefaults.putInt(KEY_LTE_PLUS_THRESHOLD_BANDWIDTH_KHZ_INT, 20000); sDefaults.putInt(KEY_NR_ADVANCED_THRESHOLD_BANDWIDTH_KHZ_INT, 0); + sDefaults.putBoolean(KEY_INCLUDE_LTE_FOR_NR_ADVANCED_THRESHOLD_BANDWIDTH_BOOL, false); sDefaults.putIntArray(KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY, new int[]{CARRIER_NR_AVAILABILITY_NSA, CARRIER_NR_AVAILABILITY_SA}); sDefaults.putBoolean(KEY_LTE_ENABLED_BOOL, true); |