diff options
121 files changed, 3146 insertions, 1070 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6ecd38f054aa..3391698ee15a 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -335,6 +335,11 @@ java_aconfig_library { aconfig_declarations: "android.os.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.mediaprovider", + ], } cc_aconfig_library { @@ -716,6 +721,7 @@ aconfig_declarations { name: "android.credentials.flags-aconfig", package: "android.credentials.flags", srcs: ["core/java/android/credentials/flags.aconfig"], + exportable: true, } java_aconfig_library { @@ -724,6 +730,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.credentials.flags-aconfig-java-export", + aconfig_declarations: "android.credentials.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", +} + // Content Protection aconfig_declarations { name: "android.view.contentprotection.flags-aconfig", diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 1cbec3126aac..66ec865092f7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -450,6 +450,11 @@ public final class SystemServiceRegistry { new CachedServiceFetcher<VcnManager>() { @Override public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { + if (!ctx.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE); IVcnManagementService service = IVcnManagementService.Stub.asInterface(b); return new VcnManager(ctx, service); @@ -1736,6 +1741,13 @@ public final class SystemServiceRegistry { return fetcher; } + private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx, + @NonNull String featureName) { + PackageManager manager = ctx.getPackageManager(); + if (manager == null) return true; + return manager.hasSystemFeature(featureName); + } + /** * Gets a system service from a given context. * @hide @@ -1758,12 +1770,18 @@ public final class SystemServiceRegistry { case Context.VIRTUALIZATION_SERVICE: case Context.VIRTUAL_DEVICE_SERVICE: return null; + case Context.VCN_MANAGEMENT_SERVICE: + if (!hasSystemFeatureOpportunistic(ctx, + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + break; case Context.SEARCH_SERVICE: // Wear device does not support SEARCH_SERVICE so we do not print WTF here - PackageManager manager = ctx.getPackageManager(); - if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) { return null; } + break; } Slog.wtf(TAG, "Manager wrapper not available: " + name); return null; diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 3304475df89f..ec59cf61097b 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -972,6 +972,7 @@ public final class VirtualDeviceManager { * * @param config camera configuration. * @return newly created camera. + * @throws UnsupportedOperationException if virtual camera isn't supported on this device. * @see VirtualDeviceParams#POLICY_TYPE_CAMERA */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 47edba6a9e56..16ca31f27028 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -47,6 +47,7 @@ flag { name: "configurable_selector_ui_enabled" description: "Enables OEM configurable Credential Selector UI" bug: "319448437" + is_exported: true } flag { diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 13d5c7e74e4b..6f901d7ec7d2 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2800,7 +2800,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 7145501c718d..69b1c34a1da2 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -3091,7 +3091,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index b214da227a2d..689e343bcbc6 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -173,7 +173,7 @@ public final class DeviceState { public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17; /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN, @@ -197,7 +197,7 @@ public final class DeviceState { public @interface DeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN @@ -207,7 +207,7 @@ public final class DeviceState { public @interface PhysicalDeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP, PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL, diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index bbda0684f1d8..cd486d0e7c2e 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -211,7 +211,7 @@ public abstract class WallpaperService extends Service { * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L; static final class WallpaperCommand { diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index aff1d4a4ee12..5366a4d16754 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -126,3 +126,10 @@ flag { description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" } + +flag { + name: "handwriting_unsupported_message" + namespace: "text" + description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it" + bug: "297962571" +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 29c83509dbf2..192b2ec93ce0 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -34,7 +34,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Editor; import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; @@ -223,7 +225,24 @@ public class HandwritingInitiator { View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null && candidateView.isEnabled()) { - if (candidateView == getConnectedOrFocusedView()) { + if (shouldShowHandwritingUnavailableMessageForView(candidateView)) { + int messagesResId = (candidateView instanceof TextView tv + && tv.isAnyPasswordInputType()) + ? R.string.error_handwriting_unsupported_password + : R.string.error_handwriting_unsupported; + Toast.makeText(candidateView.getContext(), messagesResId, + Toast.LENGTH_SHORT).show(); + if (!candidateView.hasFocus()) { + requestFocusWithoutReveal(candidateView); + } + mImm.showSoftInput(candidateView, 0); + mState.mHandled = true; + mState.mShouldInitHandwriting = false; + motionEvent.setAction((motionEvent.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK) + | MotionEvent.ACTION_CANCEL); + candidateView.getRootView().dispatchTouchEvent(motionEvent); + } else if (candidateView == getConnectedOrFocusedView()) { if (!mInitiateWithoutConnection && !candidateView.hasFocus()) { requestFocusWithoutReveal(candidateView); } @@ -484,6 +503,15 @@ public class HandwritingInitiator { return view.isStylusHandwritingAvailable(); } + private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) { + return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view); + } + + private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView( + @NonNull View view) { + return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view); + } + /** * Returns the pointer icon for the motion event, or null if it doesn't specify the icon. * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a @@ -491,7 +519,7 @@ public class HandwritingInitiator { */ public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { final View hoverView = findHoverView(event); - if (hoverView == null) { + if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) { return null; } @@ -594,7 +622,7 @@ public class HandwritingInitiator { /** * Given the location of the stylus event, return the best candidate view to initialize - * handwriting mode. + * handwriting mode or show the handwriting unavailable error message. * * @param x the x coordinates of the stylus event, in the coordinates of the window. * @param y the y coordinates of the stylus event, in the coordinates of the window. @@ -610,7 +638,8 @@ public class HandwritingInitiator { Rect handwritingArea = mTempRect; if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea) && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover) - && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) { + && shouldTriggerHandwritingOrShowUnavailableMessageForView( + connectedOrFocusedView)) { if (!isHover && mState != null) { mState.mStylusDownWithinEditorBounds = contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); @@ -628,7 +657,7 @@ public class HandwritingInitiator { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) - || !shouldTriggerStylusHandwritingForView(view)) { + || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) { continue; } @@ -856,7 +885,7 @@ public class HandwritingInitiator { /** The helper method to check if the given view is still active for handwriting. */ private static boolean isViewActive(@Nullable View view) { return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() - && view.shouldInitiateHandwriting(); + && view.shouldTrackHandwritingArea(); } private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) { diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index a2f767d002f4..07d05a4ff1ea 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -75,12 +75,14 @@ per-file View.java = file:/graphics/java/android/graphics/OWNERS per-file View.java = file:/services/core/java/com/android/server/input/OWNERS per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS per-file View.java = file:/core/java/android/view/inputmethod/OWNERS +per-file View.java = file:/core/java/android/text/OWNERS per-file ViewRootImpl.java = file:/services/accessibility/OWNERS per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS +per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0a75f4e6d731..41bfb24884a2 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12695,7 +12695,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (getSystemGestureExclusionRects().isEmpty() && collectPreferKeepClearRects().isEmpty() && collectUnrestrictedPreferKeepClearRects().isEmpty() - && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) { + && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); info.mPositionUpdateListener = null; @@ -13062,7 +13062,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void updateHandwritingArea() { // If autoHandwritingArea is not enabled, do nothing. - if (!shouldInitiateHandwriting()) return; + if (!shouldTrackHandwritingArea()) return; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this); @@ -13080,6 +13080,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the handwriting initiator should track the handwriting area for this view, + * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the + * handwriting unsupported message. + * @hide + */ + public boolean shouldTrackHandwritingArea() { + return shouldInitiateHandwriting(); + } + + /** * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this * view's bounds. The callback will be called from the UI thread. * diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0373539c44ea..52604702f2c1 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -13565,6 +13565,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ @Override + public boolean shouldTrackHandwritingArea() { + // The handwriting initiator tracks all editable TextViews regardless of whether handwriting + // is supported, so that it can show an error message for unsupported editable TextViews. + return super.shouldTrackHandwritingArea() + || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor()); + } + + /** @hide */ + @Override public boolean isStylusHandwritingAvailable() { if (mTextOperationUser == null) { return super.isStylusHandwritingAvailable(); diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 52237989f059..d48cdc4645c6 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -161,6 +161,7 @@ static struct { jfieldID mMixType; jfieldID mCallbackFlags; jfieldID mToken; + jfieldID mVirtualDeviceId; } gAudioMixFields; static jclass gAudioFormatClass; @@ -2312,7 +2313,7 @@ static jint convertAudioMixFromNative(JNIEnv *env, jobject *jAudioMix, const Aud jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str()); *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat, nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType, - deviceAddress, jBinderToken); + deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId); return AUDIO_JAVA_SUCCESS; } @@ -2347,6 +2348,7 @@ static jint convertAudioMixToNative(JNIEnv *env, AudioMix *nAudioMix, const jobj aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong); nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get()); + nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId); jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria)); env->DeleteLocalRef(jRule); @@ -3676,7 +3678,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixCstor = GetMethodIDOrDie(env, audioMixClass, "<init>", "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/" - "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V"); + "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V"); } gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule", "Landroid/media/audiopolicy/AudioMixingRule;"); @@ -3689,6 +3691,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I"); gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I"); gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;"); + gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I"); jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat"); gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass); diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index f915f038dc0d..a3dba48bbb7d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -231,8 +231,10 @@ <string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string> <!-- Displayed to tell the user that emergency calls might not be available. --> <string name="EmergencyCallWarningTitle">Emergency calling unavailable</string> - <!-- Displayed to tell the user that emergency calls might not be available. --> - <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string> + <!-- Displayed to tell the user that emergency calls might not be available; this is shown to + the user when only WiFi calling is available and the carrier does not support emergency + calls over WiFi calling. --> + <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string> <!-- Telephony notification channel name for a channel containing network alert notifications. --> <string name="notification_channel_network_alert">Alerts</string> @@ -3247,6 +3249,12 @@ <!-- Title for EditText context menu [CHAR LIMIT=20] --> <string name="editTextMenuTitle">Text actions</string> + <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string> + + <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string> + <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="input_method_nav_back_button_desc">Back</string> <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 668a88c4370a..c2fa297ea984 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3122,6 +3122,8 @@ <!-- TextView --> <java-symbol type="bool" name="config_textShareSupported" /> <java-symbol type="string" name="failed_to_copy_to_clipboard" /> + <java-symbol type="string" name="error_handwriting_unsupported" /> + <java-symbol type="string" name="error_handwriting_unsupported_password" /> <java-symbol type="id" name="notification_material_reply_container" /> <java-symbol type="id" name="notification_material_reply_text_1" /> diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index a5c962412024..6c00fd80c5e1 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -55,6 +55,7 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -72,6 +73,7 @@ import org.mockito.ArgumentCaptor; */ @Presubmit @SmallTest +@UiThreadTest @RunWith(AndroidJUnit4.class) public class HandwritingInitiatorTest { private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout(); diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 1ccc7d8084a6..5f25d70acf7c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -181,6 +182,12 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 6f7024ae76b4..1fe3c2ecec29 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -5453,7 +5453,8 @@ public class AudioManager { String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(), policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(), policy.isVolumeController(), - projection == null ? null : projection.getProjection()); + projection == null ? null : projection.getProjection(), + policy.getAttributionSource()); if (regId == null) { return ERROR; } else { diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 447d3bbddceb..80e57193d0dc 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -789,7 +789,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() { AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat); MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null) + AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext) .setMediaProjection(projection) .addMix(audioMix).build(); @@ -853,7 +853,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, .setFormat(mFormat) .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build(); + AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build(); if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 194da217a121..73deb17d0055 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -1353,7 +1353,8 @@ public class AudioTrack extends PlayerBase .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); AudioPolicy audioPolicy = - new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build(); + new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build(); + if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 98bd3caf3f7d..e612645fb4d7 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -18,6 +18,7 @@ package android.media; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; +import android.content.AttributionSource; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioFormat; @@ -361,7 +362,8 @@ interface IAudioService { String registerAudioPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, in IMediaProjection projection); + boolean isVolumeController, in IMediaProjection projection, + in AttributionSource attributionSource); oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb); diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index ab7c27f70e05..2d7db5e6ed94 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -35,6 +35,7 @@ import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.IHwBinder; import android.os.Looper; import android.os.Message; @@ -43,7 +44,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.util.Log; -import android.util.Singleton; import com.android.internal.util.FrameworkStatsLog; @@ -264,71 +264,107 @@ public final class MediaCas implements AutoCloseable { public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; - private static final Singleton<IMediaCasService> sService = - new Singleton<IMediaCasService>() { + private static IMediaCasService sService = null; + private static Object sAidlLock = new Object(); + + /** DeathListener for AIDL service */ + private static IBinder.DeathRecipient sDeathListener = + new IBinder.DeathRecipient() { @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get AIDL service"); - IMediaCasService serviceAidl = - IMediaCasService.Stub.asInterface( - ServiceManager.waitForDeclaredService( - IMediaCasService.DESCRIPTOR + "/default")); - if (serviceAidl != null) { - return serviceAidl; - } - } catch (Exception eAidl) { - Log.d(TAG, "Failed to get cas AIDL service"); + public void binderDied() { + synchronized (sAidlLock) { + Log.d(TAG, "The service is dead"); + sService.asBinder().unlinkToDeath(sDeathListener, 0); + sService = null; } - return null; } }; - private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = - new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { - @Override - protected android.hardware.cas.V1_0.IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService( - true /*wait*/); - if (serviceV12 != null) { - return serviceV12; - } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); + static IMediaCasService getService() { + synchronized (sAidlLock) { + if (sService == null || !sService.asBinder().isBinderAlive()) { + try { + Log.d(TAG, "Trying to get AIDL service"); + sService = + IMediaCasService.Stub.asInterface( + ServiceManager.waitForDeclaredService( + IMediaCasService.DESCRIPTOR + "/default")); + if (sService != null) { + sService.asBinder().linkToDeath(sDeathListener, 0); } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + } + return sService; + } + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService( - true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null; + private static Object sHidlLock = new Object(); + + /** Used to indicate the right end-point to handle the serviceDied method */ + private static final long MEDIA_CAS_HIDL_COOKIE = 394; + + /** DeathListener for HIDL service */ + private static IHwBinder.DeathRecipient sDeathListenerHidl = + new IHwBinder.DeathRecipient() { + @Override + public void serviceDied(long cookie) { + if (cookie == MEDIA_CAS_HIDL_COOKIE) { + synchronized (sHidlLock) { + sServiceHidl = null; } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); } + } + }; - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + synchronized (sHidlLock) { + if (sServiceHidl != null) { + return sServiceHidl; + } else { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); + if (serviceV12 != null) { + sServiceHidl = serviceV12; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; } - - return null; + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); } - }; - static IMediaCasService getService() { - return sService.get(); - } + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); + if (serviceV11 != null) { + sServiceHidl = serviceV11; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); + } - static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { - return sServiceHidl.get(); + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + sServiceHidl = + android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + if (sServiceHidl != null) { + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + } + return sServiceHidl; + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } + } + } + // Couldn't find an HIDL service, returning null. + return null; } private void validateInternalStates() { @@ -756,7 +792,7 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); @@ -765,7 +801,7 @@ public final class MediaCas implements AutoCloseable { } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { return serviceHidl.isSystemIdSupported(CA_system_id); @@ -781,7 +817,7 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); @@ -794,10 +830,11 @@ public final class MediaCas implements AutoCloseable { } return results; } catch (RemoteException e) { + Log.e(TAG, "Some exception while enumerating plugins"); } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index a53a8ce79354..e4eaaa317b3d 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; @@ -67,12 +68,19 @@ public class AudioMix implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* + // The (virtual) device ID that this AudioMix was registered for. This value is overwritten + // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an + // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies + // audio routing for this device ID. + private int mVirtualDeviceId; + /** * All parameters are guaranteed valid through the Builder. */ private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format, int routeFlags, int callbackFlags, - int deviceType, @Nullable String deviceAddress, IBinder token) { + int deviceType, @Nullable String deviceAddress, IBinder token, + int virtualDeviceId) { mRule = Objects.requireNonNull(rule); mFormat = Objects.requireNonNull(format); mRouteFlags = routeFlags; @@ -81,6 +89,7 @@ public class AudioMix implements Parcelable { mDeviceSystemType = deviceType; mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress; mToken = token; + mVirtualDeviceId = virtualDeviceId; } // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined @@ -269,6 +278,11 @@ public class AudioMix implements Parcelable { } /** @hide */ + public boolean matchesVirtualDeviceId(int deviceId) { + return mVirtualDeviceId == deviceId; + } + + /** @hide */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -311,6 +325,7 @@ public class AudioMix implements Parcelable { mFormat.writeToParcel(dest, flags); mRule.writeToParcel(dest, flags); dest.writeStrongBinder(mToken); + dest.writeInt(mVirtualDeviceId); } public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() { @@ -331,6 +346,7 @@ public class AudioMix implements Parcelable { mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p)); mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p)); mixBuilder.setToken(p.readStrongBinder()); + mixBuilder.setVirtualDeviceId(p.readInt()); return mixBuilder.build(); } @@ -339,6 +355,15 @@ public class AudioMix implements Parcelable { } }; + /** + * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered + * through. + * @hide + */ + public void setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + } + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) @@ -354,6 +379,7 @@ public class AudioMix implements Parcelable { private int mRouteFlags = 0; private int mCallbackFlags = 0; private IBinder mToken = null; + private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* private int mDeviceSystemType = AudioSystem.DEVICE_NONE; private String mDeviceAddress = null; @@ -404,6 +430,15 @@ public class AudioMix implements Parcelable { /** * @hide + * Only used by AudioMix internally. + */ + Builder setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + return this; + } + + /** + * @hide * Only used by AudioPolicyConfig, not a public API. * @param callbackFlags which callbacks are called from native * @return the same Builder instance. @@ -570,7 +605,7 @@ public class AudioMix implements Parcelable { } return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType, - mDeviceAddress, mToken); + mDeviceAddress, mToken, mVirtualDeviceId); } private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) { diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 508c0a2b9a21..293a8f89fbca 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -27,6 +27,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; @@ -146,6 +147,16 @@ public class AudioPolicy { return mProjection; } + /** @hide */ + public AttributionSource getAttributionSource() { + return getAttributionSource(mContext); + } + + private static AttributionSource getAttributionSource(Context context) { + return context == null + ? AttributionSource.myAttributionSource() : context.getAttributionSource(); + } + /** * The parameters are guaranteed non-null through the Builder */ @@ -208,6 +219,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } mMixes.add(mix); return this; } @@ -358,6 +372,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in attachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } @@ -400,6 +417,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in detachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 6dc07b29a510..4aa67c17ad98 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -20,6 +20,7 @@ android_library { static_libs: [ "com.google.android.material_material", "SettingsLibSettingsTheme", + "android.os.flags-aconfig-java-export", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml index 80f6b7683269..303e20c2497e 100644 --- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml +++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml @@ -18,5 +18,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.profileselector"> - <uses-sdk android:minSdkVersion="23" /> + <uses-sdk android:minSdkVersion="29" /> </manifest> diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml index 68d4047a497c..76ccb651969b 100644 --- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml +++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml @@ -21,4 +21,6 @@ <string name="settingslib_category_personal">Personal</string> <!-- Header for items under the work user [CHAR LIMIT=30] --> <string name="settingslib_category_work">Work</string> + <!-- Header for items under the private profile user [CHAR LIMIT=30] --> + <string name="settingslib_category_private">Private</string> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java index be5753beea4e..c52386bef07b 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java @@ -16,31 +16,77 @@ package com.android.settingslib.widget; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; +import android.content.pm.UserProperties; +import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArrayMap; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.core.os.BuildCompat; import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; +import com.android.settingslib.widget.profileselector.R; + import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import com.android.settingslib.widget.profileselector.R; + +import java.util.ArrayList; +import java.util.List; /** * Base fragment class for profile settings. */ public abstract class ProfileSelectFragment extends Fragment { + private static final String TAG = "ProfileSelectFragment"; + // UserHandle#USER_NULL is a @TestApi so is not accessible. + private static final int USER_NULL = -10000; + private static final int DEFAULT_POSITION = 0; + + /** + * The type of profile tab of {@link ProfileSelectFragment} to show + * <ul> + * <li>0: Personal tab. + * <li>1: Work profile tab. + * </ul> + * + * <p> Please note that this is supported for legacy reasons. Please use + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead. + */ + public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab"; + + /** + * An {@link ArrayList} of users to show. The supported users are: System user, the managed + * profile user, and the private profile user. A client should pass all the user ids that need + * to be shown in this list. Note that if this list is not provided then, for legacy reasons + * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the + * System user and one for the managed profile user. + * + * <p>Please note that this MUST be used in conjunction with + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} + */ + public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids"; /** - * Personal or Work profile tab of {@link ProfileSelectFragment} - * <p>0: Personal tab. - * <p>1: Work profile tab. + * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user + * types are supported: + * <ul> + * <li> System user. + * <li> Managed profile user. + * <li> Private profile user. + * </ul> + * + * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}. */ - public static final String EXTRA_SHOW_FRAGMENT_TAB = - ":settings:show_fragment_tab"; + public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id"; /** * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB @@ -48,13 +94,23 @@ public abstract class ProfileSelectFragment extends Fragment { public static final int PERSONAL_TAB = 0; /** - * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile */ public static final int WORK_TAB = 1; + /** + * Please note that private profile is available from API LEVEL + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be + * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only. + */ + private static final int PRIVATE_TAB = 2; + private ViewGroup mContentView; private ViewPager2 mViewPager; + private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>(); + private boolean mUsingUserIds = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -67,7 +123,7 @@ public abstract class ProfileSelectFragment extends Fragment { if (titleResId > 0) { activity.setTitle(titleResId); } - final int selectedTab = getTabId(activity, getArguments()); + initProfileTabsToShow(); final View tabContainer = mContentView.findViewById(R.id.tab_container); mViewPager = tabContainer.findViewById(R.id.view_pager); @@ -78,16 +134,14 @@ public abstract class ProfileSelectFragment extends Fragment { ).attach(); tabContainer.setVisibility(View.VISIBLE); - final TabLayout.Tab tab = tabs.getTabAt(selectedTab); + final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments())); tab.select(); return mContentView; } /** - * create Personal or Work profile fragment - * <p>0: Personal profile. - * <p>1: Work profile. + * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID} */ public abstract Fragment createFragment(int position); @@ -99,21 +153,90 @@ public abstract class ProfileSelectFragment extends Fragment { return 0; } - int getTabId(Activity activity, Bundle bundle) { + int getSelectedTabPosition(Activity activity, Bundle bundle) { if (bundle != null) { + final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL); + if (extraUserId != USER_NULL) { + return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId)); + } final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1); if (extraTab != -1) { return extraTab; } } - return PERSONAL_TAB; + return DEFAULT_POSITION; + } + + int getTabCount() { + return mUsingUserIds ? mProfileTabsByUsers.size() : 2; + } + + void initProfileTabsToShow() { + Bundle bundle = getArguments(); + if (bundle != null) { + ArrayList<Integer> userIdsToShow = + bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS); + if (userIdsToShow != null && !userIdsToShow.isEmpty()) { + mUsingUserIds = true; + UserManager userManager = getContext().getSystemService(UserManager.class); + List<UserHandle> userHandles = userManager.getUserProfiles(); + for (UserHandle userHandle : userHandles) { + if (!userIdsToShow.contains(userHandle.getIdentifier())) { + continue; + } + if (userHandle.isSystem()) { + mProfileTabsByUsers.put(userHandle, PERSONAL_TAB); + } else if (userManager.isManagedProfile(userHandle.getIdentifier())) { + mProfileTabsByUsers.put(userHandle, WORK_TAB); + } else if (shouldShowPrivateProfileIfItsOne(userHandle)) { + mProfileTabsByUsers.put(userHandle, PRIVATE_TAB); + } + } + } + } + } + + private int getProfileTabForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position; + } + + int getUserIdForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position; } private CharSequence getPageTitle(int position) { - if (position == WORK_TAB) { + int tab = getProfileTabForPosition(position); + if (tab == WORK_TAB) { return getContext().getString(R.string.settingslib_category_work); + } else if (tab == PRIVATE_TAB) { + return getContext().getString(R.string.settingslib_category_private); } return getString(R.string.settingslib_category_personal); } + + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) { + UserProperties userProperties = userManager.getUserProperties(userHandle); + return !userManager.isQuietModeEnabled(userHandle) + || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + + // It's sufficient to have this method marked with the appropriate API level because we expect + // to be here only for this API level - when then private profile was introduced. + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) { + if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) { + return false; + } + try { + Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.isPrivateProfile() + && shouldShowUserInQuietMode(userHandle, userManager); + } catch (IllegalStateException exception) { + Log.i(TAG, "Ignoring this user as the calling package not available in this user."); + } + return false; + } } diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java index f5ab64742992..37f4f275cfe7 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java @@ -18,7 +18,6 @@ package com.android.settingslib.widget; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; -import com.android.settingslib.widget.profileselector.R; /** * ViewPager Adapter to handle between TabLayout and ViewPager2 @@ -34,11 +33,11 @@ public class ProfileViewPagerAdapter extends FragmentStateAdapter { @Override public Fragment createFragment(int position) { - return mParentFragments.createFragment(position); + return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position)); } @Override public int getItemCount() { - return 2; + return mParentFragments.getTabCount(); } } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 6546b87c8802..f70ad9ed58b0 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -23,10 +23,10 @@ import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_QU import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS; import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT; -import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; @@ -77,6 +77,8 @@ public class AccessibilityMenuServiceTest { private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; private static final int TIMEOUT_UI_CHANGE_S = 5; private static final int NO_GLOBAL_ACTION = -1; + private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU) + .setPackage(PACKAGE_NAME); private static Instrumentation sInstrumentation; private static UiAutomation sUiAutomation; @@ -152,9 +154,6 @@ public class AccessibilityMenuServiceTest { @Before public void setup() throws Throwable { sOpenBlocked.set(false); - wakeUpScreen(); - sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); - openMenu(); } @After @@ -188,24 +187,17 @@ public class AccessibilityMenuServiceTest { } private static void openMenu() throws Throwable { - openMenu(false); - } - - private static void openMenu(boolean abandonOnBlock) throws Throwable { - Intent intent = new Intent(INTENT_TOGGLE_MENU); - intent.setPackage(PACKAGE_NAME); - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); TestUtils.waitUntil("Timed out before menu could appear.", TIMEOUT_UI_CHANGE_S, () -> { - if (sOpenBlocked.get() && abandonOnBlock) { - throw new IllegalStateException(); - } if (isMenuVisible()) { return true; } else { - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); return false; } }); @@ -249,6 +241,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustBrightness() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); DisplayManager displayManager = context.getSystemService( DisplayManager.class); @@ -264,22 +257,28 @@ public class AccessibilityMenuServiceTest { context.getDisplayId()).getBrightnessInfo(); try { - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum); TestUtils.waitUntil("Could not change to minimum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMinimum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMinimum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMinimum; + }); brightnessUpButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect an increase in brightness.", TIMEOUT_UI_CHANGE_S, () -> displayManager.getBrightness(context.getDisplayId()) > brightnessInfo.brightnessMinimum); - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum); TestUtils.waitUntil("Could not change to maximum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMaximum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMaximum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMaximum; + }); brightnessDownButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect a decrease in brightness.", TIMEOUT_UI_CHANGE_S, @@ -292,6 +291,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustVolume() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); AudioManager audioManager = context.getSystemService(AudioManager.class); int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); @@ -332,6 +332,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAssistantButton_opensVoiceAssistant() throws Throwable { + openMenu(); AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal())); Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND); @@ -349,6 +350,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable { + openMenu(); AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal())); Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); @@ -364,6 +366,7 @@ public class AccessibilityMenuServiceTest { @Test public void testPowerButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal())); @@ -376,6 +379,7 @@ public class AccessibilityMenuServiceTest { @Test public void testRecentButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal())); @@ -388,6 +392,7 @@ public class AccessibilityMenuServiceTest { @Test public void testLockButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal())); @@ -400,6 +405,7 @@ public class AccessibilityMenuServiceTest { @Test public void testQuickSettingsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal())); @@ -412,6 +418,7 @@ public class AccessibilityMenuServiceTest { @Test public void testNotificationsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal())); @@ -424,6 +431,7 @@ public class AccessibilityMenuServiceTest { @Test public void testScreenshotButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal())); @@ -436,6 +444,7 @@ public class AccessibilityMenuServiceTest { @Test public void testOnScreenLock_closesMenu() throws Throwable { + openMenu(); closeScreen(); wakeUpScreen(); @@ -447,13 +456,18 @@ public class AccessibilityMenuServiceTest { closeScreen(); wakeUpScreen(); - boolean blocked = false; - try { - openMenu(true); - } catch (IllegalStateException e) { - // Expected - blocked = true; - } - assertThat(blocked).isTrue(); + TestUtils.waitUntil("Did not receive signal that menu cannot open", + TIMEOUT_UI_CHANGE_S, + () -> { + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); + return sOpenBlocked.get(); + }); + } + + private static void unlockSignal() { + // MENU unlocks screen, + // BACK closes any menu that may appear if the screen wasn't locked. + sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); + sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8da50216f13c..ad09febd74a9 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -424,6 +424,13 @@ flag { } flag { + name: "screenshot_shelf_ui" + namespace: "systemui" + description: "Use new shelf UI flow for screenshots" + bug: "329659738" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index 621ddf796f58..1da6c1ee6638 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -53,6 +53,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,6 +72,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import com.android.compose.PlatformButton import com.android.compose.animation.scene.ElementKey @@ -84,7 +86,9 @@ import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel @@ -166,7 +170,7 @@ private fun StandardLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier, ) @@ -228,7 +232,7 @@ private fun SplitLayout( when (authMethod) { is PinBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -241,7 +245,7 @@ private fun SplitLayout( } is PatternBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -280,7 +284,7 @@ private fun SplitLayout( modifier = Modifier.fillMaxWidth().align(Alignment.Center), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -376,7 +380,7 @@ private fun BesideUserSwitcherLayout( modifier = Modifier.fillMaxWidth() ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -441,7 +445,7 @@ private fun BelowUserSwitcherLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -548,26 +552,44 @@ private fun SceneScope.FoldableScene( @Composable private fun StatusMessage( - viewModel: BouncerViewModel, + viewModel: BouncerMessageViewModel, modifier: Modifier = Modifier, ) { - val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState() + val message: MessageViewModel? by viewModel.message.collectAsState() + + DisposableEffect(Unit) { + viewModel.onShown() + onDispose {} + } Crossfade( targetState = message, label = "Bouncer message", - animationSpec = if (message.isUpdateAnimated) tween() else snap(), + animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(), modifier = modifier.fillMaxWidth(), - ) { - Box( - contentAlignment = Alignment.Center, + ) { msg -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), ) { - Text( - text = it.text, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge, - ) + msg?.let { + Text( + text = it.text, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.size(10.dp)) + Text( + text = it.secondaryText ?: "", + color = MaterialTheme.colorScheme.onSurface, + fontSize = 14.sp, + lineHeight = 20.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index 2a13d4931b69..c34f2fd26d0c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -74,10 +74,7 @@ internal fun PasswordBouncer( val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState() val selectedUserId by viewModel.selectedUserId.collectAsState() - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } LaunchedEffect(animateFailure) { if (animateFailure) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index 0a5f5d281f83..a78c2c0d16c6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -72,10 +72,7 @@ internal fun PatternBouncer( centerDotsVertically: Boolean, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index f505b9067140..5651a4646b2d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -72,10 +72,7 @@ fun PinPad( verticalSpacing: Dp, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 82e19e7c154c..1d86b15dbf4f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -58,7 +58,6 @@ constructor( if (currentClock?.smallClock?.view == null) { return } - viewModel.clock = currentClock val context = LocalContext.current MovableElement(key = smallClockElementKey, modifier = modifier) { @@ -89,7 +88,6 @@ constructor( @Composable fun SceneScope.LargeClock(modifier: Modifier = Modifier) { val currentClock by viewModel.currentClock.collectAsState() - viewModel.clock = currentClock if (currentClock?.largeClock?.view == null) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c9b271b342c..525ad161c94f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,45 +16,33 @@ package com.android.systemui.keyguard.ui.composable.section -import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.scene.shared.flag.SceneContainerFlags -import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher @SysUISingleton class NotificationSection @Inject constructor( - @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, sceneContainerFlags: SceneContainerFlags, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, ) { init { @@ -73,24 +61,13 @@ constructor( sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) } - SharedNotificationContainerBinder.bind( + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainImmediateDispatcher, ) if (sceneContainerFlags.isEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainImmediateDispatcher, - ) + notificationStackViewBinder.bindWhileAttached() } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 8ad2bb78f5d6..9ba5e3b846ed 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -70,10 +71,10 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -140,6 +141,7 @@ fun SceneScope.NotificationScrollingStack( ) { val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current + val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) val scrollState = rememberScrollState() val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) val expansionFraction by viewModel.expandFraction.collectAsState(0f) @@ -157,7 +159,7 @@ fun SceneScope.NotificationScrollingStack( val contentHeight = viewModel.intrinsicContentHeight.collectAsState() - val stackRounding = viewModel.stackRounding.collectAsState() + val stackRounding = viewModel.stackRounding.collectAsState(StackRounding()) // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is // calculated in minScrimOffset. The scrim is the same height as the screen minus the @@ -225,6 +227,7 @@ fun SceneScope.NotificationScrollingStack( .graphicsLayer { shape = calculateCornerRadius( + scrimCornerRadius, screenCornerRadius, { expansionFraction }, layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) @@ -357,6 +360,7 @@ private fun SceneScope.NotificationPlaceholder( } private fun calculateCornerRadius( + scrimCornerRadius: Dp, screenCornerRadius: Dp, expansionFraction: () -> Float, transitioning: Boolean, @@ -364,12 +368,12 @@ private fun calculateCornerRadius( return if (transitioning) { lerp( start = screenCornerRadius.value, - stop = SCRIM_CORNER_RADIUS, + stop = scrimCornerRadius.value, fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f), ) .dp } else { - SCRIM_CORNER_RADIUS.dp + scrimCornerRadius } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 2c31f9b6b92d..85798acd0dcd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -370,7 +370,8 @@ private fun SceneScope.SplitShade( NotificationScrollingStack( viewModel = viewModel.notifications, maxScrimTop = { 0f }, - modifier = Modifier.weight(1f).fillMaxHeight(), + modifier = + Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight), ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index af51cee2a255..dc3b612d3594 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -73,7 +73,7 @@ internal class Scene( internal class SceneScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, -) : SceneScope { +) : SceneScope, ElementStateScope by layoutImpl.elementStateScope { override val layoutState: SceneTransitionLayoutState = layoutImpl.state override fun Modifier.element(key: ElementKey): Modifier { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index b7e2dd13f321..ebc90990275d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -131,9 +131,30 @@ interface SceneTransitionLayoutScope { */ @DslMarker annotation class ElementDsl +/** A scope that can be used to query the target state of an element or scene. */ +interface ElementStateScope { + /** + * Return the *target* size of [this] element in the given [scene], i.e. the size of the element + * when idle, or `null` if the element is not composed and measured in that scene (yet). + */ + fun ElementKey.targetSize(scene: SceneKey): IntSize? + + /** + * Return the *target* offset of [this] element in the given [scene], i.e. the size of the + * element when idle, or `null` if the element is not composed and placed in that scene (yet). + */ + fun ElementKey.targetOffset(scene: SceneKey): Offset? + + /** + * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if + * the scene was never composed. + */ + fun SceneKey.targetSize(): IntSize? +} + @Stable @ElementDsl -interface BaseSceneScope { +interface BaseSceneScope : ElementStateScope { /** The state of the [SceneTransitionLayout] in which this scene is contained. */ val layoutState: SceneTransitionLayoutState @@ -415,25 +436,7 @@ interface UserActionDistance { ): Float } -interface UserActionDistanceScope : Density { - /** - * Return the *target* size of [this] element in the given [scene], i.e. the size of the element - * when idle, or `null` if the element is not composed and measured in that scene (yet). - */ - fun ElementKey.targetSize(scene: SceneKey): IntSize? - - /** - * Return the *target* offset of [this] element in the given [scene], i.e. the size of the - * element when idle, or `null` if the element is not composed and placed in that scene (yet). - */ - fun ElementKey.targetOffset(scene: SceneKey): Offset? - - /** - * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if - * the scene was never composed. - */ - fun SceneKey.targetSize(): IntSize? -} +interface UserActionDistanceScope : Density, ElementStateScope /** The user action has a fixed [absoluteDistance]. */ class FixedDistance(private val distance: Dp) : UserActionDistance { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 25b0895fafb3..b1cfdcf07977 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -98,6 +98,7 @@ internal class SceneTransitionLayoutImpl( private val horizontalDraggableHandler: DraggableHandlerImpl private val verticalDraggableHandler: DraggableHandlerImpl + internal val elementStateScope = ElementStateScopeImpl(this) private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope get() = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 228d19f09cff..b7abb33c1242 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -19,15 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize -internal class UserActionDistanceScopeImpl( +internal class ElementStateScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, -) : UserActionDistanceScope { - override val density: Float - get() = layoutImpl.density.density - - override val fontScale: Float - get() = layoutImpl.density.fontScale - +) : ElementStateScope { override fun ElementKey.targetSize(scene: SceneKey): IntSize? { return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf { it != Element.SizeUnspecified @@ -44,3 +38,13 @@ internal class UserActionDistanceScopeImpl( return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero } } } + +internal class UserActionDistanceScopeImpl( + private val layoutImpl: SceneTransitionLayoutImpl, +) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope { + override val density: Float + get() = layoutImpl.density.density + + override val fontScale: Float + get() = layoutImpl.density.fontScale +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 707777b9f728..b0d03b15d310 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -71,34 +71,6 @@ class BouncerInteractorTest : SysuiTestCase() { } @Test - fun pinAuthMethod() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - runCurrent() - underTest.clearMessage() - assertThat(message).isNull() - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Wrong input. - assertThat(underTest.authenticate(listOf(9, 8, 7))) - .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Correct input. - assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) - .isEqualTo(AuthenticationResult.SUCCEEDED) - } - - @Test fun pinAuthMethod_sim_skipsAuthentication() = testScope.runTest { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -146,8 +118,6 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() = testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) @@ -156,7 +126,6 @@ class BouncerInteractorTest : SysuiTestCase() { // Incomplete input. assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() // Correct input. assertThat( @@ -166,28 +135,19 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() } @Test fun passwordAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) - // Wrong input. assertThat(underTest.authenticate("alohamora".toList())) .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) // Too short input. assertThat( @@ -201,7 +161,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) // Correct input. assertThat(underTest.authenticate("password".toList())) @@ -211,13 +170,10 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun patternAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Wrong input. val wrongPattern = @@ -231,10 +187,6 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(wrongPattern.size) .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength) assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Too short input. val tooShortPattern = @@ -244,10 +196,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) assertThat(underTest.authenticate(tooShortPattern)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Correct input. assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)) @@ -258,7 +206,6 @@ class BouncerInteractorTest : SysuiTestCase() { fun lockoutStarted() = testScope.runTest { val lockoutStartedEvents by collectValues(underTest.onLockoutStarted) - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin @@ -272,17 +219,14 @@ class BouncerInteractorTest : SysuiTestCase() { .isEqualTo(AuthenticationResult.FAILED) if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { assertThat(lockoutStartedEvents).isEmpty() - assertThat(message).isNotEmpty() } } assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) - assertThat(message).isNull() // Advance the time to finish the lockout: advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds) assertThat(authenticationInteractor.lockoutEndTimestamp).isNull() - assertThat(message).isNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) // Trigger lockout again: diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 701b7039a1ed..c878e0b4757d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.bouncer.domain.interactor import android.content.pm.UserInfo -import android.os.Handler import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,27 +27,25 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FaceSensorInfo -import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.shared.model.BouncerMessageModel -import com.android.systemui.bouncer.ui.BouncerView -import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper -import com.android.systemui.keyguard.DismissCallbackRegistry -import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.res.R.string.kg_trust_agent_disabled -import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -61,7 +58,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -70,34 +66,22 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class BouncerMessageInteractorTest : SysuiTestCase() { - + private val kosmos = testKosmos() private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java) private val repository = BouncerMessageRepositoryImpl() - private val userRepository = FakeUserRepository() - private val fakeTrustRepository = FakeTrustRepository() - private val fakeFacePropertyRepository = FakeFacePropertyRepository() - private val bouncerRepository = FakeKeyguardBouncerRepository() - private val fakeDeviceEntryFingerprintAuthRepository = - FakeDeviceEntryFingerprintAuthRepository() - private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val biometricSettingsRepository: FakeBiometricSettingsRepository = - FakeBiometricSettingsRepository() + private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository + private val testScope = kosmos.testScope @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor @Mock private lateinit var securityModel: KeyguardSecurityModel @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor - private lateinit var testScope: TestScope private lateinit var underTest: BouncerMessageInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) - userRepository.setUserInfos(listOf(PRIMARY_USER)) - testScope = TestScope() + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) allowTestableLooperAsMainThread() whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) @@ -105,44 +89,28 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } suspend fun TestScope.init() { - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) - primaryBouncerInteractor = - PrimaryBouncerInteractor( - bouncerRepository, - mock(BouncerView::class.java), - mock(Handler::class.java), - mock(KeyguardStateController::class.java), - mock(KeyguardSecurityModel::class.java), - mock(PrimaryBouncerCallbackInteractor::class.java), - mock(FalsingCollector::class.java), - mock(DismissCallbackRegistry::class.java), - context, - keyguardUpdateMonitor, - fakeTrustRepository, - testScope.backgroundScope, - mSelectedUserInteractor, - mock(DeviceEntryFaceAuthInteractor::class.java), - ) underTest = BouncerMessageInteractor( repository = repository, - userRepository = userRepository, + userRepository = kosmos.fakeUserRepository, countDownTimerUtil = countDownTimerUtil, updateMonitor = updateMonitor, biometricSettingsRepository = biometricSettingsRepository, - applicationScope = this.backgroundScope, - trustRepository = fakeTrustRepository, + applicationScope = testScope.backgroundScope, + trustRepository = kosmos.fakeTrustRepository, systemPropertiesHelper = systemPropertiesHelper, - primaryBouncerInteractor = primaryBouncerInteractor, - facePropertyRepository = fakeFacePropertyRepository, - deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository, - faceAuthRepository = fakeDeviceEntryFaceAuthRepository, + primaryBouncerInteractor = kosmos.primaryBouncerInteractor, + facePropertyRepository = kosmos.fakeFacePropertyRepository, + deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor, + faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository, securityModel = securityModel ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) - bouncerRepository.setPrimaryShow(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true) runCurrent() } @@ -268,7 +236,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -276,7 +244,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("Can’t unlock with face. Too many attempts.") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -289,15 +257,17 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -311,14 +281,14 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockedOutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockedOutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockedOutMessage)) @@ -327,6 +297,19 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + init() + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + val lockedOutMessage by collectLastValue(underTest.bouncerMessage) + + runCurrent() + + assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") + } + + @Test fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = testScope.runTest { init() @@ -344,9 +327,10 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() = testScope.runTest { init() - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() val defaultMessage = Pair("Enter PIN", null) @@ -377,12 +361,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() - fakeTrustRepository.setCurrentUserTrustManaged(true) - fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) val defaultMessage = Pair("Enter PIN", null) @@ -415,8 +400,8 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) @@ -453,12 +438,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to @@ -466,6 +452,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index d30e33332926..c9fa671ad34f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -48,6 +48,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt new file mode 100644 index 000000000000..16ec9aa897fb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.bouncer.ui.viewmodel + +import android.content.pm.UserInfo +import android.hardware.biometrics.BiometricFaceConstants +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin +import com.android.systemui.biometrics.data.repository.FaceSensorInfo +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BouncerMessageViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private lateinit var underTest: BouncerMessageViewModel + + @Before + fun setUp() { + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) + kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true + underTest = kosmos.bouncerMessageViewModel + overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable") + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + } + + @Test + fun message_defaultMessage_basedOnAuthMethod() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() + + assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint") + } + + @Test + fun message() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(message?.isUpdateAnimated).isTrue() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { + bouncerInteractor.authenticate(WRONG_PIN) + } + assertThat(message?.isUpdateAnimated).isFalse() + + val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + advanceTimeBy(lockoutEndMs - testScope.currentTime) + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun lockoutMessage() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() + runCurrent() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> + bouncerInteractor.authenticate(WRONG_PIN) + runCurrent() + if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { + assertThat(message?.text).isEqualTo("Wrong PIN. Try again.") + assertThat(message?.isUpdateAnimated).isTrue() + } + } + val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS + assertTryAgainMessage(message?.text, lockoutSeconds) + assertThat(message?.isUpdateAnimated).isFalse() + + repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> + advanceTimeBy(1.seconds) + val remainingSeconds = lockoutSeconds - time - 1 + if (remainingSeconds > 0) { + assertTryAgainMessage(message?.text, remainingSeconds) + } + } + assertThat(message?.text).isEqualTo("Enter PIN") + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + runCurrent() + + val defaultMessage = Pair("Enter PIN", null) + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Enter PIN", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Unlock with PIN or fingerprint", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun onFingerprintLockout_messageUpdated() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val lockedOutMessage by collectLastValue(underTest.message) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockedOutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + val message by collectLastValue(underTest.message) + + runCurrent() + + assertThat(message?.text).isEqualTo("Enter PIN") + } + + @Test + fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update") + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "Device updated. Enter PIN to continue.") + ) + } + + @Test + fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK)) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun setFingerprintMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + runCurrent() + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + FailFingerprintAuthenticationStatus + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + } + + @Test + fun setFaceMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + runCurrent() + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_TIMEOUT, + "Try again" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + FailedFaceAuthenticationStatus() + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Face not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + } + + private fun TestScope.verifyMessagesForAuthFlags( + vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>> + ) { + val actualMessage by collectLastValue(underTest.message) + + authFlagToMessagePair.forEach { (flag, expectedMessagePair) -> + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag) + ) + runCurrent() + + assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first) + + if (expectedMessagePair.second == null) { + assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue() + } else { + assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second) + } + } + } + + private fun assertTryAgainMessage( + message: String?, + time: Int, + ) { + assertThat(message).contains("Try again in $time second") + } + + companion object { + private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 73db1757c06a..3afca96e07a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest @@ -142,54 +141,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun message() = - testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(message?.isUpdateAnimated).isTrue() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(message?.isUpdateAnimated).isFalse() - - val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - advanceTimeBy(lockoutEndMs - testScope.currentTime) - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test - fun lockoutMessage() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> - bouncerInteractor.authenticate(WRONG_PIN) - if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - assertThat(message?.text).isEqualTo(bouncerInteractor.message.value) - assertThat(message?.isUpdateAnimated).isTrue() - } - } - val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - assertTryAgainMessage(message?.text, lockoutSeconds) - assertThat(message?.isUpdateAnimated).isFalse() - - repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> - advanceTimeBy(1.seconds) - val remainingSeconds = lockoutSeconds - time - 1 - if (remainingSeconds > 0) { - assertTryAgainMessage(message?.text, remainingSeconds) - } - } - assertThat(message?.text).isEmpty() - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test fun isInputEnabled() = testScope.runTest { val isInputEnabled by @@ -212,25 +163,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun dialogViewModel() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val dialogViewModel by collectLastValue(underTest.dialogViewModel) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - assertThat(dialogViewModel).isNull() - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(dialogViewModel).isNotNull() - assertThat(dialogViewModel?.text).isNotEmpty() - - dialogViewModel?.onDismiss?.invoke() - assertThat(dialogViewModel).isNull() - } - - @Test fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) @@ -265,13 +197,6 @@ class BouncerViewModelTest : SysuiTestCase() { return listOf(None, Pin, Password, Pattern, Sim) } - private fun assertTryAgainMessage( - message: String?, - time: Int, - ) { - assertThat(message).isEqualTo("Try again in $time seconds.") - } - companion object { private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index df50eb64f8b6..71c578545647 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -66,7 +66,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor } private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private val isInputEnabled = MutableStateFlow(true) private val underTest = @@ -76,6 +75,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = {}, ) @Before @@ -88,11 +88,9 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password) @@ -101,16 +99,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onHidden_resetsPasswordInputAndMessage() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isNotEmpty() underTest.onHidden() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() } @@ -118,13 +113,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onPasswordInputChanged() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() assertThat(password).isEqualTo("password") assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -144,7 +137,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenWrong() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -152,13 +144,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) } @Test fun onAuthenticateKeyPressed_whenEmpty() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password @@ -171,14 +161,12 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) } @Test fun onAuthenticateKeyPressed_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -186,12 +174,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onPasswordInputChanged("wrong") underTest.onAuthenticateKeyPressed() assertThat(password).isEqualTo("") - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) assertThat(authResult).isFalse() // Enter the correct password: underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() underTest.onAuthenticateKeyPressed() @@ -331,10 +317,8 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 91a056ddd685..51b73ee92df5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -63,6 +63,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), + onIntentionalUserInput = {}, ) } @@ -79,12 +80,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN) assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -95,14 +94,12 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragStart() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() - assertThat(message?.text).isEmpty() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -148,7 +145,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -159,7 +155,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -302,7 +297,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenPatternTooShort() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel) lockDeviceAndOpenPatternBouncer() @@ -325,7 +319,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() - assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN) assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull() } } @@ -334,7 +327,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -344,7 +336,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(authResult).isFalse() // Enter the correct pattern: @@ -370,10 +361,8 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 7b75a3715415..564795429fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -56,7 +56,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private lateinit var underTest: PinBouncerViewModel @Before @@ -69,6 +68,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) @@ -78,11 +78,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() - assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN) assertThat(pin).isEmpty() assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin) } @@ -98,6 +96,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) assertThat(underTest.isSimAreaVisible).isTrue() @@ -126,6 +125,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) @@ -136,20 +136,17 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() underTest.onPinButtonClicked(1) - assertThat(message?.text).isEmpty() assertThat(pin).containsExactly(1) } @Test fun onBackspaceButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -158,7 +155,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonClicked() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() } @@ -183,7 +179,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onBackspaceButtonLongPressed() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -195,7 +190,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonLongPressed() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -217,7 +211,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -230,7 +223,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -238,7 +230,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -248,13 +239,11 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onAuthenticateButtonClicked() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(pin).isEmpty() assertThat(authResult).isFalse() // Enter the correct PIN: FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) - assertThat(message?.text).isEmpty() underTest.onAuthenticateButtonClicked() @@ -277,7 +266,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAutoConfirm_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) lockDeviceAndOpenPinBouncer() @@ -290,7 +278,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) // PIN is now wrong! assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -390,10 +377,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 8e2e94716660..a7e98ea34154 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -18,10 +18,16 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.os.UserHandle import android.provider.Settings import android.widget.RemoteViews +import androidx.activity.result.ActivityResultLauncher import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -39,6 +45,7 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.view.MediaHost @@ -46,15 +53,19 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -64,6 +75,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var providerInfo: AppWidgetProviderInfo + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -73,6 +86,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository + private val testableResources = context.orCreateTestableResources + private lateinit var underTest: CommunalEditModeViewModel @Before @@ -96,6 +111,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { mediaHost, uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), + kosmos.testDispatcher, ) } @@ -217,7 +233,69 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + @Test + fun onOpenWidgetPicker_launchesWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher).launch(any()) + assertTrue(success) + } + } + + @Test + fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher, never()).launch(any()) + assertFalse(success) + } + } + + @Test + fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + whenever(activityResultLauncher.launch(any())) + .thenThrow(ActivityNotFoundException::class.java) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher, + ) + + assertFalse(success) + } + } + private companion object { val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt index decbdaf0feee..51f99570b51e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt @@ -26,12 +26,10 @@ import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthR import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlin.test.Test -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { @@ -59,17 +57,20 @@ class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { } @Test - fun isSensorUnderDisplay_trueForUdfpsSensorTypes() = + fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() = testScope.runTest { - val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val isFingerprintCurrentlyAllowedInBouncer by + collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer) fingerprintPropertyRepository.supportsUdfps() - assertThat(isSensorUnderDisplay).isTrue() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse() fingerprintPropertyRepository.supportsRearFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() fingerprintPropertyRepository.supportsSideFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index 6dd425c2afbc..e3fa89c5760d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -20,9 +20,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test @@ -30,10 +33,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class NotificationStackAppearanceInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val underTest = kosmos.notificationStackAppearanceInteractor @@ -59,6 +61,18 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { assertThat(stackBounds).isEqualTo(bounds2) } + @Test + fun stackRounding() = + testScope.runTest { + val stackRounding by collectLastValue(underTest.stackRounding) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false)) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true)) + } + @Test(expected = IllegalStateException::class) fun setStackBounds_withImproperBounds_throwsException() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt new file mode 100644 index 000000000000..a5ad3c325e51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.DisposableHandle +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisposableHandlesTest : SysuiTestCase() { + @Test + fun disposeWorksOnce() { + var handleDisposeCount = 0 + val underTest = DisposableHandles() + + // Add a handle + underTest += DisposableHandle { handleDisposeCount++ } + + // dispose() calls dispose() on children + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + + // Once disposed, children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + } + + @Test + fun replaceCallsDispose() { + var handleDisposeCount1 = 0 + var handleDisposeCount2 = 0 + val underTest = DisposableHandles() + val handle1 = DisposableHandle { handleDisposeCount1++ } + val handle2 = DisposableHandle { handleDisposeCount2++ } + + // First add handle1 + underTest += handle1 + + // replace() calls dispose() on existing children + underTest.replaceAll(handle2) + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(0) + + // Once disposed, replaced children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(1) + } +} diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml new file mode 100644 index 000000000000..ef1a21f2fdf6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.ui.ScreenshotShelfView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/guideline"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_edit_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_scroll_chip" + android:visibility="gone" /> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/screenshot_preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="14dp" + android:elevation="8dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="parent"/> + <ImageView + android:id="@+id/screenshot_preview" + android:layout_width="@dimen/overlay_x_scale" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="8dp" + android:contentDescription="@string/screenshot_edit_description" + android:scaleType="fitEnd" + android:background="@drawable/overlay_preview_background" + android:adjustViewBounds="true" + android:clickable="true" + app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="56dp" + android:layout_height="56dp" + android:visibility="gone" + android:elevation="9dp" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> + <FrameLayout + android:id="@+id/screenshot_dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="11dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintBottom_toTopOf="@id/screenshot_preview" + android:contentDescription="@string/screenshot_dismiss_description"> + <ImageView + android:id="@+id/screenshot_dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOnPrimary" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> + <ImageView + android:id="@+id/screenshot_scrollable_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="matrix" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + android:elevation="7dp"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="0dp" /> + + <FrameLayout + android:id="@+id/screenshot_message_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginTop="4dp" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="450dp" + app:layout_constraintHorizontal_bias="0"> + <include layout="@layout/screenshot_work_profile_first_run" /> + <include layout="@layout/screenshot_detection_notice" /> + </FrameLayout> +</com.android.systemui.screenshot.ui.ScreenshotShelfView> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index bf5eeb9e8294..e48959c2b91a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -235,6 +235,8 @@ <string name="screenshot_edit_label">Edit</string> <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_edit_description">Edit screenshot</string> + <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] --> + <string name="screenshot_share_label">Share</string> <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_share_description">Share screenshot</string> <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index e621ffe4cbc4..86f64f809e42 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -631,7 +631,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Nullable public ClockController getClock() { if (migrateClocksToBlueprint()) { - return mKeyguardClockInteractor.getClock(); + return mKeyguardClockInteractor.getCurrentClock().getValue(); } else { return mClockEventController.getClock(); } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt index d849b3a44519..94e085479675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt @@ -20,7 +20,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow /** Provides access to bouncer-related application state. */ @SysUISingleton @@ -29,9 +28,6 @@ class BouncerRepository constructor( private val flags: FeatureFlagsClassic, ) { - /** The user-facing message to show in the bouncer. */ - val message = MutableStateFlow<String?>(null) - /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index d8be1afc4dd6..aeb564d53195 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -16,13 +16,8 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.Context import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.interactor.AuthenticationResult -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.classifier.FalsingClassifier @@ -31,7 +26,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -41,7 +35,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch /** Encapsulates business logic and application state accessing use-cases. */ @SysUISingleton @@ -49,16 +42,14 @@ class BouncerInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, - @Application private val applicationContext: Context, private val repository: BouncerRepository, private val authenticationInteractor: AuthenticationInteractor, private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, - private val simBouncerInteractor: SimBouncerInteractor, ) { - /** The user-facing message to show in the bouncer when lockout is not active. */ - val message: StateFlow<String?> = repository.message + private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>() + val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput /** Whether the auto confirm feature is enabled for the currently-selected user. */ val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled @@ -119,25 +110,6 @@ constructor( ) } - fun setMessage(message: String?) { - repository.message.value = message - } - - /** - * Resets the user-facing message back to the default according to the current authentication - * method. - */ - fun resetMessage() { - applicationScope.launch { - setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) - } - } - - /** Removes the user-facing message. */ - fun clearMessage() { - setMessage(null) - } - /** * Attempts to authenticate based on the given user input. * @@ -176,50 +148,17 @@ constructor( .async { authenticationInteractor.authenticate(input, tryAutoConfirm) } .await() - if (authenticationInteractor.lockoutEndTimestamp != null) { - clearMessage() - } else if ( + if ( authResult == AuthenticationResult.FAILED || (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm) ) { - showWrongInputMessage() + _onIncorrectBouncerInput.emit(Unit) } return authResult } - /** - * Shows the a message notifying the user that their credentials input is wrong. - * - * Callers should use this instead of [authenticate] when they know ahead of time that an auth - * attempt will fail but aren't interested in the other side effects like triggering lockout. - * For example, if the user entered a pattern that's too short, the system can show the error - * message without having the attempt trigger lockout. - */ - private suspend fun showWrongInputMessage() { - setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod())) - } - /** Notifies that the input method editor (software keyboard) has been hidden by the user. */ suspend fun onImeHiddenByUser() { _onImeHiddenByUser.emit(Unit) } - - private fun promptMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Sim -> simBouncerInteractor.getDefaultMessage() - is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin) - is Password -> applicationContext.getString(R.string.keyguard_enter_your_password) - is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern) - else -> "" - } - } - - private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Pin -> applicationContext.getString(R.string.kg_wrong_pin) - is Password -> applicationContext.getString(R.string.kg_wrong_password) - is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern) - else -> "" - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index 7f6fc914e92b..d20c60724822 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -33,15 +33,17 @@ import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.Quint +import com.android.systemui.util.kotlin.Sextuple +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -56,6 +58,7 @@ private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" private const val TAG = "BouncerMessageInteractor" /** Handles business logic for the primary bouncer message area. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class BouncerMessageInteractor @Inject @@ -63,23 +66,24 @@ constructor( private val repository: BouncerMessageRepository, private val userRepository: UserRepository, private val countDownTimerUtil: CountDownTimerUtil, - private val updateMonitor: KeyguardUpdateMonitor, + updateMonitor: KeyguardUpdateMonitor, trustRepository: TrustRepository, biometricSettingsRepository: BiometricSettingsRepository, private val systemPropertiesHelper: SystemPropertiesHelper, primaryBouncerInteractor: PrimaryBouncerInteractor, @Application private val applicationScope: CoroutineScope, private val facePropertyRepository: FacePropertyRepository, - deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, faceAuthRepository: DeviceEntryFaceAuthRepository, private val securityModel: KeyguardSecurityModel, ) { - private val isFingerprintAuthCurrentlyAllowed = - deviceEntryFingerprintAuthRepository.isLockedOut - .isFalse() - .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed) - .stateIn(applicationScope, SharingStarted.Eagerly, false) + private val isFingerprintAuthCurrentlyAllowedOnBouncer = + deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn( + applicationScope, + SharingStarted.Eagerly, + false + ) private val currentSecurityMode get() = securityModel.getSecurityMode(currentUserId) @@ -99,13 +103,13 @@ constructor( BiometricSourceType.FACE -> BouncerMessageStrings.incorrectFaceInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() else -> BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } @@ -144,11 +148,12 @@ constructor( biometricSettingsRepository.authenticationFlags, trustRepository.isCurrentUserTrustManaged, isAnyBiometricsEnabledAndEnrolled, - deviceEntryFingerprintAuthRepository.isLockedOut, + deviceEntryFingerprintAuthInteractor.isLockedOut, faceAuthRepository.isLockedOut, - ::Quint + isFingerprintAuthCurrentlyAllowedOnBouncer, + ::Sextuple ) - .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) -> + .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) -> val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value val trustOrBiometricsAvailable = (isTrustUsuallyManaged || biometricsEnrolledAndEnabled) @@ -193,14 +198,14 @@ constructor( } else { BouncerMessageStrings.faceLockedOut( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) { BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if ( @@ -209,19 +214,19 @@ constructor( ) { BouncerMessageStrings.nonStrongAuthTimeout( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) { @@ -265,7 +270,7 @@ constructor( repository.setMessage( BouncerMessageStrings.incorrectSecurityInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() ) @@ -274,14 +279,22 @@ constructor( fun setFingerprintAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } fun setFaceAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -289,7 +302,11 @@ constructor( if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -297,7 +314,7 @@ constructor( get() = BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() @@ -355,11 +372,6 @@ open class CountDownTimerUtil @Inject constructor() { private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) = this.combine(anotherFlow) { a, b -> a || b } -private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) = - this.combine(anotherFlow) { a, b -> a && b } - -private fun Flow<Boolean>.isFalse() = this.map { !it } - private fun defaultMessage( securityMode: SecurityMode, secondaryMessage: String?, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt index f3903ded7cf4..aebc50f92e8d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui import android.app.AlertDialog import android.content.Context +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,6 +31,7 @@ import dagger.Provides includes = [ BouncerViewModelModule::class, + BouncerMessageViewModelModule::class, ], ) interface BouncerViewModule { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index 0d7f6dcce1c7..4fbf735a62a2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -57,17 +57,11 @@ sealed class AuthMethodBouncerViewModel( */ @get:StringRes abstract val lockoutMessageId: Int - /** Notifies that the UI has been shown to the user. */ - fun onShown() { - interactor.resetMessage() - } - /** * Notifies that the UI has been hidden from the user (after any transitions have completed). */ open fun onHidden() { clearInput() - interactor.resetMessage() } /** Notifies that the user has placed down a pointer. */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt new file mode 100644 index 000000000000..6cb9b16e2f9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.bouncer.ui.viewmodel + +import android.content.Context +import android.util.PluralsMessageFormatter +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.bouncer.shared.model.BouncerMessagePair +import com.android.systemui.bouncer.shared.model.BouncerMessageStrings +import com.android.systemui.bouncer.shared.model.primaryMessage +import com.android.systemui.bouncer.shared.model.secondaryMessage +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason +import com.android.systemui.deviceentry.shared.model.FaceFailureMessage +import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel +import com.android.systemui.user.ui.viewmodel.UserViewModel +import com.android.systemui.util.kotlin.Utils.Companion.sample +import com.android.systemui.util.time.SystemClock +import dagger.Module +import dagger.Provides +import kotlin.math.ceil +import kotlin.math.max +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** Holds UI state for the 2-line status message shown on the bouncer. */ +@OptIn(ExperimentalCoroutinesApi::class) +class BouncerMessageViewModel( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + private val bouncerInteractor: BouncerInteractor, + private val simBouncerInteractor: SimBouncerInteractor, + private val authenticationInteractor: AuthenticationInteractor, + selectedUser: Flow<UserViewModel>, + private val clock: SystemClock, + private val biometricMessageInteractor: BiometricMessageInteractor, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, +) { + /** + * A message shown when the user has attempted the wrong credential too many times and now must + * wait a while before attempting to authenticate again. + * + * This is updated every second (countdown) during the lockout. When lockout is not active, this + * is `null` and no lockout message should be shown. + */ + private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Whether there is a lockout message that is available to be shown in the status message. */ + val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null } + + /** The user-facing message to show in the bouncer. */ + val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Initializes the bouncer message to default whenever it is shown. */ + fun onShown() { + showDefaultMessage() + } + + /** Reset the message shown on the bouncer to the default message. */ + fun showDefaultMessage() { + resetToDefault.tryEmit(Unit) + } + + private val resetToDefault = MutableSharedFlow<Unit>(replay = 1) + + private var lockoutCountdownJob: Job? = null + + private fun defaultBouncerMessageInitializer() { + applicationScope.launch { + resetToDefault.emit(Unit) + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + resetToDefault.map { + MessageViewModel(simBouncerInteractor.getDefaultMessage()) + } + } else if (authMethod.isSecure) { + combine( + deviceEntryInteractor.deviceEntryRestrictionReason, + lockoutMessage, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + resetToDefault, + ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> + lockoutMsg + ?: deviceEntryRestrictedReason.toMessage( + authMethod, + isFpAllowedInBouncer + ) + } + } else { + emptyFlow() + } + } + .collectLatest { messageViewModel -> message.value = messageViewModel } + } + } + + private fun listenForSimBouncerEvents() { + // Listen for any events from the SIM bouncer and update the message shown on the bouncer. + applicationScope.launch { + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + simBouncerInteractor.bouncerMessageChanged.map { simMsg -> + simMsg?.let { MessageViewModel(it) } + } + } else { + emptyFlow() + } + } + .collectLatest { + if (it != null) { + message.value = it + } else { + resetToDefault.emit(Unit) + } + } + } + } + + private fun listenForFaceMessages() { + // Listen for any events from face authentication and update the message shown on the + // bouncer. + applicationScope.launch { + biometricMessageInteractor.faceMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + ) + .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> + val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage( + authMethod, + fingerprintAllowedOnBouncer + ) + .primaryMessage + .toResString() + message.value = + when (faceMessage) { + is FaceTimeoutMessage -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = true + ) + is FaceLockoutMessage -> + if (isFaceAuthStrong) + BouncerMessageStrings.class3AuthLockedOut(authMethod) + .toMessage() + else + BouncerMessageStrings.faceLockedOut( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + is FaceFailureMessage -> + BouncerMessageStrings.incorrectFaceInput( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForFingerprintMessages() { + applicationScope.launch { + // Listen for any events from fingerprint authentication and update the message shown + // on the bouncer. + biometricMessageInteractor.fingerprintMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) + .primaryMessage + .toResString() + message.value = + when (fingerprintMessage) { + is FingerprintLockoutMessage -> + BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() + is FingerprintFailureMessage -> + BouncerMessageStrings.incorrectFingerprintInput(authMethod) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = fingerprintMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForBouncerEvents() { + // Keeps the lockout message up-to-date. + applicationScope.launch { + bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } + } + + // Listens to relevant bouncer events + applicationScope.launch { + bouncerInteractor.onIncorrectBouncerInput + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (_, authMethod, isFingerprintAllowed) -> + message.emit( + BouncerMessageStrings.incorrectSecurityInput( + authMethod, + isFingerprintAllowed + ) + .toMessage() + ) + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun DeviceEntryRestrictionReason?.toMessage( + authMethod: AuthenticationMethodModel, + isFingerprintAllowedOnBouncer: Boolean, + ): MessageViewModel { + return when (this) { + DeviceEntryRestrictionReason.UserLockdown -> + BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> + BouncerMessageStrings.authRequiredAfterReboot(authMethod) + DeviceEntryRestrictionReason.PolicyLockdown -> + BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod) + DeviceEntryRestrictionReason.UnattendedUpdate -> + BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> + BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod) + DeviceEntryRestrictionReason.SecurityTimeout -> + BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod) + DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> + BouncerMessageStrings.class3AuthLockedOut(authMethod) + DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> + BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> + BouncerMessageStrings.nonStrongAuthTimeout( + authMethod, + isFingerprintAllowedOnBouncer + ) + DeviceEntryRestrictionReason.TrustAgentDisabled -> + BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.AdaptiveAuthRequest -> + BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( + authMethod, + isFingerprintAllowedOnBouncer + ) + else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer) + }.toMessage() + } + + private fun BouncerMessagePair.toMessage(): MessageViewModel { + val primaryMsg = this.primaryMessage.toResString() + val secondaryMsg = + if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString() + return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true) + } + + /** Shows the countdown message and refreshes it every second. */ + private fun startLockoutCountdown() { + lockoutCountdownJob?.cancel() + lockoutCountdownJob = + applicationScope.launch { + authenticationInteractor.authenticationMethod.collectLatest { authMethod -> + do { + val remainingSeconds = remainingLockoutSeconds() + val authLockedOutMsg = + BouncerMessageStrings.primaryAuthLockedOut(authMethod) + lockoutMessage.value = + if (remainingSeconds > 0) { + MessageViewModel( + text = + kg_too_many_failed_attempts_countdown.toPluralString( + mutableMapOf<String, Any>( + Pair("count", remainingSeconds) + ) + ), + secondaryText = authLockedOutMsg.secondaryMessage.toResString(), + isUpdateAnimated = false + ) + } else { + null + } + delay(1.seconds) + } while (remainingSeconds > 0) + lockoutCountdownJob = null + } + } + } + + private fun remainingLockoutSeconds(): Int { + val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) + return ceil(remainingMs / 1000f).toInt() + } + + private fun Int.toPluralString(formatterArgs: Map<String, Any>): String = + PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this) + + private fun Int.toResString(): String = applicationContext.getString(this) + + init { + if (flags.isComposeBouncerOrSceneContainerEnabled()) { + applicationScope.launch { + // Update the lockout countdown whenever the selected user is switched. + selectedUser.collect { startLockoutCountdown() } + } + + defaultBouncerMessageInitializer() + + listenForSimBouncerEvents() + listenForBouncerEvents() + listenForFaceMessages() + listenForFingerprintMessages() + } + } + + companion object { + private const val MESSAGE_DURATION = 2000L + } +} + +/** Data class that represents the status message show on the bouncer. */ +data class MessageViewModel( + val text: String, + val secondaryText: String? = null, + /** + * Whether updates to the message should be cross-animated from one message to another. + * + * If `false`, no animation should be applied, the message text should just be replaced + * instantly. + */ + val isUpdateAnimated: Boolean = true, +) + +@OptIn(ExperimentalCoroutinesApi::class) +@Module +object BouncerMessageViewModelModule { + + @Provides + @SysUISingleton + fun viewModel( + @Application applicationContext: Context, + @Application applicationScope: CoroutineScope, + bouncerInteractor: BouncerInteractor, + simBouncerInteractor: SimBouncerInteractor, + authenticationInteractor: AuthenticationInteractor, + clock: SystemClock, + biometricMessageInteractor: BiometricMessageInteractor, + faceAuthInteractor: DeviceEntryFaceAuthInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, + userSwitcherViewModel: UserSwitcherViewModel, + ): BouncerMessageViewModel { + return BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + clock = clock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = faceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = fingerprintInteractor, + flags = flags, + selectedUser = userSwitcherViewModel.selectedUser, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 62875783ef5f..5c07cc57c620 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -21,7 +21,6 @@ import android.app.admin.DevicePolicyResources import android.content.Context import android.graphics.Bitmap import androidx.core.graphics.drawable.toBitmap -import com.android.internal.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel @@ -40,18 +39,12 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.user.ui.viewmodel.UserViewModel -import com.android.systemui.util.time.SystemClock import dagger.Module import dagger.Provides -import kotlin.math.ceil -import kotlin.math.max -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -72,13 +65,13 @@ class BouncerViewModel( private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, private val selectedUserInteractor: SelectedUserInteractor, + private val devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, flags: ComposeBouncerFlags, selectedUser: Flow<UserViewModel>, users: Flow<List<UserViewModel>>, userSwitcherMenu: Flow<List<UserActionViewModel>>, actionButton: Flow<BouncerActionButtonModel?>, - private val clock: SystemClock, - private val devicePolicyManager: DevicePolicyManager, ) { val selectedUserImage: StateFlow<Bitmap?> = selectedUser @@ -89,6 +82,8 @@ class BouncerViewModel( initialValue = null, ) + val message: BouncerMessageViewModel = bouncerMessageViewModel + val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = combine( users, @@ -163,24 +158,6 @@ class BouncerViewModel( ) /** - * A message shown when the user has attempted the wrong credential too many times and now must - * wait a while before attempting to authenticate again. - * - * This is updated every second (countdown) during the lockout duration. When lockout is not - * active, this is `null` and no lockout message should be shown. - */ - private val lockoutMessage = MutableStateFlow<String?>(null) - - /** The user-facing message to show in the bouncer. */ - val message: StateFlow<MessageViewModel> = - combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = createMessageViewModel(), - ) - - /** * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not * be shown. */ @@ -222,31 +199,16 @@ class BouncerViewModel( ) private val isInputEnabled: StateFlow<Boolean> = - lockoutMessage - .map { it == null } + bouncerMessageViewModel.isLockoutMessagePresent + .map { lockoutMessagePresent -> !lockoutMessagePresent } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = authenticationInteractor.lockoutEndTimestamp == null, ) - private var lockoutCountdownJob: Job? = null - init { if (flags.isComposeBouncerOrSceneContainerEnabled()) { - // Keeps the lockout dialog up-to-date. - applicationScope.launch { - bouncerInteractor.onLockoutStarted.collect { - showLockoutDialog() - startLockoutCountdown() - } - } - - applicationScope.launch { - // Update the lockout countdown whenever the selected user is switched. - selectedUser.collect { startLockoutCountdown() } - } - // Keeps the upcoming wipe dialog up-to-date. applicationScope.launch { authenticationInteractor.upcomingWipe.collect { wipeModel -> @@ -256,48 +218,6 @@ class BouncerViewModel( } } - private fun showLockoutDialog() { - applicationScope.launch { - val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value - lockoutDialogMessage.value = - authMethodViewModel.value?.lockoutMessageId?.let { messageId -> - applicationContext.getString( - messageId, - failedAttempts, - remainingLockoutSeconds() - ) - } - } - } - - /** Shows the countdown message and refreshes it every second. */ - private fun startLockoutCountdown() { - lockoutCountdownJob?.cancel() - lockoutCountdownJob = - applicationScope.launch { - do { - val remainingSeconds = remainingLockoutSeconds() - lockoutMessage.value = - if (remainingSeconds > 0) { - applicationContext.getString( - R.string.lockscreen_too_many_failed_attempts_countdown, - remainingSeconds, - ) - } else { - null - } - delay(1.seconds) - } while (remainingSeconds > 0) - lockoutCountdownJob = null - } - } - - private fun remainingLockoutSeconds(): Int { - val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) - return ceil(remainingMs / 1000f).toInt() - } - private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean { return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel } @@ -306,15 +226,6 @@ class BouncerViewModel( return authMethod !is PasswordBouncerViewModel } - private fun createMessageViewModel(): MessageViewModel { - val isLockedOut = lockoutMessage.value != null - return MessageViewModel( - // A lockout message takes precedence over the non-lockout message. - text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "", - isUpdateAnimated = !isLockedOut, - ) - } - private fun getChildViewModel( authenticationMethod: AuthenticationMethodModel, ): AuthMethodBouncerViewModel? { @@ -336,7 +247,8 @@ class BouncerViewModel( interactor = bouncerInteractor, isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, - authenticationMethod = authenticationMethod + authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Sim -> PinBouncerViewModel( @@ -346,6 +258,7 @@ class BouncerViewModel( isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Password -> PasswordBouncerViewModel( @@ -354,6 +267,7 @@ class BouncerViewModel( interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Pattern -> PatternBouncerViewModel( @@ -361,11 +275,17 @@ class BouncerViewModel( viewModelScope = newViewModelScope, interactor = bouncerInteractor, isInputEnabled = isInputEnabled, + onIntentionalUserInput = ::onIntentionalUserInput ) else -> null } } + private fun onIntentionalUserInput() { + message.showDefaultMessage() + bouncerInteractor.onIntentionalUserInput() + } + private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope { return CoroutineScope( SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher @@ -437,18 +357,6 @@ class BouncerViewModel( } } - data class MessageViewModel( - val text: String, - - /** - * Whether updates to the message should be cross-animated from one message to another. - * - * If `false`, no animation should be applied, the message text should just be replaced - * instantly. - */ - val isUpdateAnimated: Boolean, - ) - data class DialogViewModel( val text: String, @@ -480,8 +388,8 @@ object BouncerViewModelModule { selectedUserInteractor: SelectedUserInteractor, flags: ComposeBouncerFlags, userSwitcherViewModel: UserSwitcherViewModel, - clock: SystemClock, devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, ): BouncerViewModel { return BouncerViewModel( applicationContext = applicationContext, @@ -497,8 +405,8 @@ object BouncerViewModelModule { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = actionButtonInteractor.actionButton, - clock = clock, devicePolicyManager = devicePolicyManager, + bouncerMessageViewModel = bouncerMessageViewModel, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index b42eda108d54..052fb6b3c4d7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -40,6 +40,7 @@ class PasswordBouncerViewModel( viewModelScope: CoroutineScope, isInputEnabled: StateFlow<Boolean>, interactor: BouncerInteractor, + private val onIntentionalUserInput: () -> Unit, private val inputMethodInteractor: InputMethodInteractor, private val selectedUserInteractor: SelectedUserInteractor, ) : @@ -96,12 +97,8 @@ class PasswordBouncerViewModel( /** Notifies that the user has changed the password input. */ fun onPasswordInputChanged(newPassword: String) { - if (this.password.value.isEmpty() && newPassword.isNotEmpty()) { - interactor.clearMessage() - } - if (newPassword.isNotEmpty()) { - interactor.onIntentionalUserInput() + onIntentionalUserInput() } _password.value = newPassword diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 69f8032ef4f2..a4016005a756 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -40,6 +40,7 @@ class PatternBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, ) : AuthMethodBouncerViewModel( viewModelScope = viewModelScope, @@ -84,7 +85,7 @@ class PatternBouncerViewModel( /** Notifies that the user has started a drag gesture across the dot grid. */ fun onDragStart() { - interactor.clearMessage() + onIntentionalUserInput() } /** diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index e910a9271ee2..62da5c0e5675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -41,6 +41,7 @@ class PinBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, private val simBouncerInteractor: SimBouncerInteractor, authenticationMethod: AuthenticationMethodModel, ) : @@ -131,11 +132,8 @@ class PinBouncerViewModel( /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { val pinInput = mutablePinInput.value - if (pinInput.isEmpty()) { - interactor.clearMessage() - } - interactor.onIntentionalUserInput() + onIntentionalUserInput() mutablePinInput.value = pinInput.append(input) tryAuthenticate(useAutoConfirm = true) @@ -149,7 +147,6 @@ class PinBouncerViewModel( /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { clearInput() - interactor.clearMessage() } /** Notifies that the user clicked the "enter" button. */ @@ -173,7 +170,6 @@ class PinBouncerViewModel( /** Resets the sim screen and shows a default message. */ private fun onResetSimFlow() { simBouncerInteractor.resetSimPukUserInput() - interactor.resetMessage() clearInput() } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 964eb6f3a613..578389b57a99 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -54,6 +54,18 @@ constructor( } /** + * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device + * configuration. + * + * @see android.content.res.Resources.getDimensionPixelSize + */ + fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> { + return configurationController.onDensityOrFontScaleChanged.emitOnStart().map { + context.resources.getDimensionPixelOffset(id) + } + } + + /** * Returns a [Flow] that emits a color that is kept in sync with the device theme. * * @see Utils.getColorAttrDefaultColor diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index bfe751af7154..afa7c37c648e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -16,24 +16,36 @@ package com.android.systemui.communal.ui.viewmodel +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Resources +import android.util.Log +import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.dagger.MediaModule +import com.android.systemui.res.R import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -45,6 +57,7 @@ constructor( @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, @CommunalLog logBuffer: LogBuffer, + @Background private val backgroundDispatcher: CoroutineDispatcher, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { private val logger = Logger(logBuffer, "CommunalEditModeViewModel") @@ -86,10 +99,77 @@ constructor( uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } - /** Returns the widget categories to show on communal hub. */ - val getCommunalWidgetCategories: Int - get() = communalSettingsInteractor.communalWidgetCategories.value + /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */ + suspend fun onOpenWidgetPicker( + resources: Resources, + packageManager: PackageManager, + activityLauncher: ActivityResultLauncher<Intent> + ): Boolean = + withContext(backgroundDispatcher) { + val widgets = communalInteractor.widgetContent.first() + val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo } + getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let { + try { + activityLauncher.launch(it) + return@withContext true + } catch (e: Exception) { + Log.e(TAG, "Failed to launch widget picker activity", e) + } + } + false + } + + private fun getWidgetPickerActivityIntent( + resources: Resources, + packageManager: PackageManager, + excludeList: ArrayList<AppWidgetProviderInfo> + ): Intent? { + val packageName = + getLauncherPackageName(packageManager) + ?: run { + Log.e(TAG, "Couldn't resolve launcher package name") + return@getWidgetPickerActivityIntent null + } + + return Intent(Intent.ACTION_PICK).apply { + setPackage(packageName) + putExtra( + EXTRA_DESIRED_WIDGET_WIDTH, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) + ) + putExtra( + EXTRA_DESIRED_WIDGET_HEIGHT, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height) + ) + putExtra( + AppWidgetManager.EXTRA_CATEGORY_FILTER, + communalSettingsInteractor.communalWidgetCategories.value + ) + putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE) + putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList) + } + } + + private fun getLauncherPackageName(packageManager: PackageManager): String? { + return packageManager + .resolveActivity( + Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, + PackageManager.MATCH_DEFAULT_ONLY + ) + ?.activityInfo + ?.packageName + } /** Sets whether edit mode is currently open */ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) + + companion object { + private const val TAG = "CommunalEditModeViewModel" + + private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" + private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" + private const val EXTRA_UI_SURFACE_KEY = "ui_surface" + private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub" + const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index b6ad26b24dc7..ba18f0125a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,9 +16,7 @@ package com.android.systemui.communal.widgets -import android.appwidget.AppWidgetManager import android.content.Intent -import android.content.pm.PackageManager import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -32,6 +30,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.android.app.tracing.coroutines.launch import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger @@ -43,8 +43,8 @@ import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtra import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @@ -57,11 +57,8 @@ constructor( @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { - private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" - private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" - private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" - private const val TAG = "EditWidgetsActivity" + private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" const val EXTRA_PRESELECTED_KEY = "preselected_key" } @@ -136,39 +133,13 @@ constructor( } private fun onOpenWidgetPicker() { - val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } - packageManager - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) - ?.activityInfo - ?.packageName - ?.let { packageName -> - try { - addWidgetActivityLauncher.launch( - Intent(Intent.ACTION_PICK).apply { - setPackage(packageName) - putExtra( - EXTRA_DESIRED_WIDGET_WIDTH, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_width - ) - ) - putExtra( - EXTRA_DESIRED_WIDGET_HEIGHT, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_height - ) - ) - putExtra( - AppWidgetManager.EXTRA_CATEGORY_FILTER, - communalViewModel.getCommunalWidgetCategories - ) - } - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to launch widget picker activity", e) - } - } - ?: run { Log.e(TAG, "Couldn't resolve launcher package name") } + lifecycleScope.launch { + communalViewModel.onOpenWidgetPicker( + resources, + packageManager, + addWidgetActivityLauncher + ) + } } private fun onEditDone() { diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 805999397282..c4e0ef7d082d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @@ -72,4 +74,14 @@ constructor( */ val isSensorUnderDisplay = fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps) + + /** Whether fingerprint authentication is currently allowed while on the bouncer. */ + val isFingerprintCurrentlyAllowedOnBouncer = + isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay -> + if (sensorBelowDisplay) { + flowOf(false) + } else { + isFingerprintAuthCurrentlyAllowed + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index 7ad5aac63837..7a56554be1d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -113,7 +113,10 @@ constructor( override val currentClock: StateFlow<ClockController?> = currentClockId - .map { clockRegistry.createCurrentClock() } + .map { + clockEventController.clock = clockRegistry.createCurrentClock() + clockEventController.clock + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 4812e03ec3f6..89148b09b3ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -213,9 +213,10 @@ constructor( cs: ConstraintSet, viewModel: KeyguardClockViewModel ) { - if (!DEBUG || viewModel.clock == null) return + val currentClock = viewModel.currentClock.value + if (!DEBUG || currentClock == null) return val smallClockViewId = R.id.lockscreen_clock_view - val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id + val largeClockViewId = currentClock.largeClock.layout.views[0].id Log.i( TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 01596ed2e3ef..fa1fe5ec1fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.transition.TransitionManager import android.transition.TransitionSet import android.view.View.INVISIBLE +import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout @@ -40,7 +41,8 @@ import kotlinx.coroutines.launch object KeyguardClockViewBinder { private val TAG = KeyguardClockViewBinder::class.simpleName!! - + // When changing to new clock, we need to remove old clock views from burnInLayer + private var lastClock: ClockController? = null @JvmStatic fun bind( clockSection: ClockSection, @@ -55,12 +57,11 @@ object KeyguardClockViewBinder { } } keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.currentClock.collect { currentClock -> - cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) - viewModel.clock = currentClock + cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) addClockViews(currentClock, keyguardRootView) updateBurnInLayer(keyguardRootView, viewModel) applyConstraints(clockSection, keyguardRootView, true) @@ -76,7 +77,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.clockShouldBeCentered.collect { clockShouldBeCentered -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true // TODO(b/323020908): remove ID check if ( @@ -93,7 +94,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.isAodIconsVisible.collect { isAodIconsVisible -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true if ( viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" @@ -132,11 +133,14 @@ object KeyguardClockViewBinder { } private fun cleanupClockViews( - clockController: ClockController?, + currentClock: ClockController?, rootView: ConstraintLayout, burnInLayer: Layer? ) { - clockController?.let { clock -> + if (lastClock == currentClock) { + return + } + lastClock?.let { clock -> clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) rootView.removeView(it) @@ -150,6 +154,7 @@ object KeyguardClockViewBinder { } clock.largeClock.layout.views.forEach { rootView.removeView(it) } } + lastClock = currentClock } @VisibleForTesting @@ -157,11 +162,19 @@ object KeyguardClockViewBinder { clockController: ClockController?, rootView: ConstraintLayout, ) { + // We'll collect the same clock when exiting wallpaper picker without changing clock + // so we need to remove clock views from parent before addView again clockController?.let { clock -> clock.smallClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } clock.largeClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index b77f0c5a1e60..4d0a25fb7cd3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -41,7 +41,7 @@ object KeyguardSmartspaceViewBinder { blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 7c76e6afc074..f60da0e842e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.util.kotlin.DisposableHandles import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -173,7 +174,7 @@ constructor( private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null - private val disposables = mutableSetOf<DisposableHandle>() + private val disposables = DisposableHandles() private var isDestroyed = false private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() @@ -183,7 +184,7 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) - disposables.add(DisposableHandle { coroutineScope.cancel() }) + disposables += DisposableHandle { coroutineScope.cancel() } if (keyguardBottomAreaRefactor()) { quickAffordancesCombinedViewModel.enablePreviewMode( @@ -214,7 +215,7 @@ constructor( if (hostToken == null) null else InputTransferToken(hostToken), "KeyguardPreviewRenderer" ) - disposables.add(DisposableHandle { host.release() }) + disposables += DisposableHandle { host.release() } } } @@ -284,7 +285,7 @@ constructor( fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() - disposables.forEach { it.dispose() } + disposables.dispose() if (keyguardBottomAreaRefactor()) { shortcutsBindings.forEach { it.destroy() } } @@ -372,7 +373,7 @@ constructor( private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) if (!keyguardBottomAreaRefactor()) { - disposables.add( + disposables += KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, @@ -387,7 +388,6 @@ constructor( null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode ) - ) } rootView.addView( keyguardRootView, @@ -555,14 +555,12 @@ constructor( } } clockRegistry.registerClockChangeListener(clockChangeListener) - disposables.add( - DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - ) + disposables += DisposableHandle { + clockRegistry.unregisterClockChangeListener(clockChangeListener) + } clockController.registerListeners(parentView) - disposables.add(DisposableHandle { clockController.unregisterListeners() }) + disposables += DisposableHandle { clockController.unregisterListeners() } } val receiver = @@ -581,7 +579,7 @@ constructor( addAction(Intent.ACTION_TIME_CHANGED) }, ) - disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } if (!migrateClocksToBlueprint()) { val layoutChangeListener = @@ -602,9 +600,9 @@ constructor( } } parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables.add( - DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } - ) + disposables += DisposableHandle { + parentView.removeOnLayoutChangeListener(layoutChangeListener) + } } onClockChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index 9c9df806c38c..a215efa724f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -41,7 +41,7 @@ class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : Tran private fun excludeClockAndSmartspaceViews(transition: Transition) { transition.excludeTarget(SmartspaceView::class.java, true) - clockViewModel.clock?.let { clock -> + clockViewModel.currentClock.value?.let { clock -> clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 3adeb2aeb283..c69d868866d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -57,7 +57,9 @@ class IntraBlueprintTransition( when (config.type) { Type.NoTransition -> {} Type.DefaultClockStepping -> - addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) + addTransition( + clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) } + ) else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index a183b720c087..7847c1ce3968 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -86,7 +86,7 @@ constructor( if (!Flags.migrateClocksToBlueprint()) { return } - clockInteractor.clock?.let { clock -> + keyguardClockViewModel.currentClock.value?.let { clock -> constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 6a3b920f9692..c1b0cc6b6db9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -26,20 +26,16 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import dagger.Lazy import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Single column format for notifications (default for phones) */ class DefaultNotificationStackScrollLayoutSection @@ -50,12 +46,9 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, - @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( context, @@ -63,11 +56,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 5dea7cbb801d..83235020b416 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -31,16 +31,11 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle +import com.android.systemui.util.kotlin.DisposableHandles abstract class NotificationStackScrollLayoutSection constructor( @@ -49,14 +44,11 @@ constructor( private val notificationPanelView: NotificationPanelView, private val sharedNotificationContainer: SharedNotificationContainer, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - private val ambientState: AmbientState, - private val controller: NotificationStackScrollLayoutController, - private val notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val mainDispatcher: CoroutineDispatcher, + private val sharedNotificationContainerBinder: SharedNotificationContainerBinder, + private val notificationStackViewBinder: NotificationStackViewBinder, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + private val disposableHandles = DisposableHandles() /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -102,39 +94,20 @@ constructor( return } - disposeHandles() - disposableHandles.add( - SharedNotificationContainerBinder.bind( + disposableHandles.dispose() + disposableHandles += + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainDispatcher, ) - ) if (sceneContainerFlags.isEnabled()) { - disposableHandles.add( - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainDispatcher, - ) - ) + disposableHandles += notificationStackViewBinder.bindWhileAttached() } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposeHandles() + disposableHandles.dispose() constraintLayout.removeView(placeHolderId) } - - private fun disposeHandles() { - disposableHandles.forEach { it.dispose() } - disposableHandles.clear() - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 2545302ccaa1..4a705a7f849d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -24,19 +24,14 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Large-screen format for notifications, shown as two columns on the device */ class SplitShadeNotificationStackScrollLayoutSection @@ -47,12 +42,8 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, - @Main mainDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, ) : NotificationStackScrollLayoutSection( context, @@ -60,11 +51,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 6184c82cbff7..4d3a78d32b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -216,7 +216,9 @@ class ClockSizeTransition( captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled if (viewModel.useLargeClock) { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } else { addTarget(R.id.lockscreen_clock_view) } @@ -276,7 +278,9 @@ class ClockSizeTransition( if (viewModel.useLargeClock) { addTarget(R.id.lockscreen_clock_view) } else { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index f961e083e64f..9c1f0770708c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -169,7 +169,7 @@ constructor( provider: Provider<ClockController>?, ): Provider<ClockController>? { return if (Flags.migrateClocksToBlueprint()) { - Provider { keyguardClockViewModel.clock } + Provider { keyguardClockViewModel.currentClock.value } } else { provider } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index b6622e5c07b1..1c1c33ab7e7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.SettingsClockSize -import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -54,8 +53,6 @@ constructor( val useLargeClock: Boolean get() = clockSize.value == LARGE - var clock: ClockController? by keyguardClockInteractor::clock - val clockSize = combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { selectedSize, diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 5d2aeef5eb16..b34b3701528b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -432,6 +432,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { for (int i = 0; i < NP; i++) { mPages.get(i).removeAllViews(); } + if (mPageIndicator != null) { + mPageIndicator.setNumPages(numPages); + } if (NP == numPages) { return; } @@ -443,7 +446,6 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mLogger.d("Removing page"); mPages.remove(mPages.size() - 1); } - mPageIndicator.setNumPages(mPages.size()); setAdapter(mAdapter); mAdapter.notifyDataSetChanged(); if (mPageToRestore != NO_PAGE) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt new file mode 100644 index 000000000000..abdbd6880b33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.appcompat.content.res.AppCompatResources +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI + * implementation. + */ +interface ScreenshotActionsProvider { + data class ScreenshotAction( + val icon: Drawable?, + val text: String?, + val overrideTransition: Boolean, + val retrieveIntent: (Uri) -> Intent + ) + + fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent + fun getActions(context: Context, user: UserHandle): List<ScreenshotAction> +} + +class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider { + override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent { + return ActionIntentCreator.createEdit(uri, context) + } + + override fun getActions( + context: Context, + user: UserHandle + ): List<ScreenshotActionsProvider.ScreenshotAction> { + val editAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), + context.resources.getString(R.string.screenshot_edit_label), + true + ) { uri -> + ActionIntentCreator.createEdit(uri, context) + } + val shareAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), + context.resources.getString(R.string.screenshot_share_label), + false + ) { uri -> + ActionIntentCreator.createShare(uri) + } + return listOf(editAction, shareAction) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt new file mode 100644 index 000000000000..9354fd27ce5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.res.R +import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT +import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.ui.ScreenshotAnimationController +import com.android.systemui.screenshot.ui.ScreenshotShelfView +import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Controls the screenshot view and viewModel. */ +class ScreenshotShelfViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + private val viewModel: ScreenshotViewModel, + private val staticActionsProvider: ScreenshotActionsProvider, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { + override val view: ScreenshotShelfView = + LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView + override val screenshotPreview: View + override var packageName: String = "" + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + override var screenshot: ScreenshotData? = null + set(value) { + viewModel.setScreenshotBitmap(value?.bitmap) + field = value + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override var isDismissing = false + override var isPendingSharedTransition = false + + private val animationController = ScreenshotAnimationController(view) + + init { + ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" } + screenshotPreview = view.screenshotPreview + } + + override fun reset() { + animationController.cancel() + isPendingSharedTransition = false + viewModel.setScreenshotBitmap(null) + viewModel.setActions(listOf()) + } + override fun updateInsets(insets: WindowInsets) {} + override fun updateOrientation(insets: WindowInsets) {} + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + return animationController.getEntranceAnimation() + } + + override fun addQuickShareChip(quickShareAction: Notification.Action) {} + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) { + val staticActions = + staticActionsProvider.getActions(context, imageData.owner).map { + ActionButtonViewModel(it.icon, it.text) { + val intent = it.retrieveIntent(imageData.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, imageData.owner, it.overrideTransition) + } + } + + viewModel.setActions(staticActions) + } + + override fun requestDismissal(event: ScreenshotEvent) { + debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } + + // If we're already animating out, don't restart the animation + if (isDismissing) { + debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" } + return + } + logger.log(event, 0, packageName) + val animator = animationController.getExitAnimation() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + isDismissing = true + } + override fun onAnimationEnd(animator: Animator) { + isDismissing = false + callbacks?.onDismiss() + } + } + ) + animator.start() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) {} + + override fun hideScrollChip() {} + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) {} + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) {} + + override fun restoreNonScrollingUi() {} + + override fun stopInputListening() {} + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index cdb9abb15e84..9118ee1dfc73 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,16 +16,23 @@ package com.android.systemui.screenshot.dagger; +import static com.android.systemui.Flags.screenshotShelfUi; + import android.app.Service; +import android.view.accessibility.AccessibilityManager; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.DefaultScreenshotActionsProvider; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; +import com.android.systemui.screenshot.ScreenshotActionsProvider; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; +import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; @@ -34,6 +41,7 @@ import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; import dagger.Binds; import dagger.Module; @@ -85,9 +93,25 @@ public abstract class ScreenshotModule { abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + @Binds + abstract ScreenshotActionsProvider bindScreenshotActionsProvider( + DefaultScreenshotActionsProvider defaultScreenshotActionsProvider); + + @Provides + @SysUISingleton + static ScreenshotViewModel providesScreenshotViewModel( + AccessibilityManager accessibilityManager) { + return new ScreenshotViewModel(accessibilityManager); + } + @Provides static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { - return legacyScreenshotViewProxyFactory; + if (screenshotShelfUi()) { + return shelfScreenshotViewProxyFactory; + } else { + return legacyScreenshotViewProxyFactory; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt new file mode 100644 index 000000000000..2c178736d9c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View + +class ScreenshotAnimationController(private val view: View) { + private var animator: Animator? = null + + fun getEntranceAnimation(): Animator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { view.alpha = it.animatedFraction } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 0f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 1f + } + } + ) + this.animator = animator + return animator + } + + fun getExitAnimation(): Animator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.addUpdateListener { view.alpha = it.animatedValue as Float } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 1f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 0f + } + } + ) + this.animator = animator + return animator + } + + fun cancel() { + animator?.cancel() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt new file mode 100644 index 000000000000..747ad4f9e48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.res.R + +class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + lateinit var screenshotPreview: ImageView + + override fun onFinishInflate() { + super.onFinishInflate() + screenshotPreview = requireViewById(R.id.screenshot_preview) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt new file mode 100644 index 000000000000..a5825b5f7797 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel + +object ActionButtonViewBinder { + /** Binds the given view to the given view-model */ + fun bind(view: View, viewModel: ActionButtonViewModel) { + val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) + val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) + iconView.setImageDrawable(viewModel.icon) + textView.text = viewModel.name + setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false) + if (viewModel.onClicked != null) { + view.setOnClickListener { viewModel.onClicked.invoke() } + } else { + view.setOnClickListener(null) + } + view.visibility = View.VISIBLE + view.alpha = 1f + } + + private fun setMargins(iconView: View, textView: View, hasText: Boolean) { + val iconParams = iconView.layoutParams as LinearLayout.LayoutParams + val textParams = textView.layoutParams as LinearLayout.LayoutParams + if (hasText) { + iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start) + iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing) + textParams.marginStart = 0 + textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end) + } else { + val paddingHorizontal = + iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal) + iconParams.marginStart = paddingHorizontal + iconParams.marginEnd = paddingHorizontal + } + iconView.layoutParams = iconParams + textView.layoutParams = textParams + } + + private fun View.dpToPx(dimenId: Int): Int { + return this.resources.getDimensionPixelSize(dimenId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt new file mode 100644 index 000000000000..3bcd52cbc99e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.children +import kotlinx.coroutines.launch + +object ScreenshotShelfViewBinder { + fun bind( + view: ViewGroup, + viewModel: ScreenshotViewModel, + layoutInflater: LayoutInflater, + ) { + val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) + val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) + previewView.clipToOutline = true + val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) + view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility = + if (viewModel.showDismissButton) View.VISIBLE else View.GONE + + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.preview.collect { bitmap -> + if (bitmap != null) { + previewView.setImageBitmap(bitmap) + previewView.visibility = View.VISIBLE + previewBorder.visibility = View.VISIBLE + } else { + previewView.visibility = View.GONE + previewBorder.visibility = View.GONE + } + } + } + launch { + viewModel.actions.collect { actions -> + if (actions.isNotEmpty()) { + view + .requireViewById<View>(R.id.actions_container_background) + .visibility = View.VISIBLE + } + val viewPool = actionsContainer.children.toList() + actionsContainer.removeAllViews() + val actionButtons = + List(actions.size) { + viewPool.getOrElse(it) { + layoutInflater.inflate( + R.layout.overlay_action_chip, + actionsContainer, + false + ) + } + } + actionButtons.zip(actions).forEach { + actionsContainer.addView(it.first) + ActionButtonViewBinder.bind(it.first, it.second) + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt new file mode 100644 index 000000000000..6ee970534352 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.drawable.Drawable + +data class ActionButtonViewModel( + val icon: Drawable?, + val name: String?, + val onClicked: (() -> Unit)? +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt new file mode 100644 index 000000000000..3a652d90bb78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.Bitmap +import android.view.accessibility.AccessibilityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { + private val _preview = MutableStateFlow<Bitmap?>(null) + val preview: StateFlow<Bitmap?> = _preview + private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) + val actions: StateFlow<List<ActionButtonViewModel>> = _actions + val showDismissButton: Boolean + get() = accessibilityManager.isEnabled + + fun setScreenshotBitmap(bitmap: Bitmap?) { + _preview.value = bitmap + } + + fun setActions(actions: List<ActionButtonViewModel>) { + _actions.value = actions + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index 9fffb66ac831..79ba25e1e23e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.stack.data.repository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds -import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -29,10 +28,6 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** The bounds of the notification stack in the current scene. */ val stackBounds = MutableStateFlow(StackBounds()) - /** The whether the corners of the notification stack should be rounded */ - // TODO: replace with the logic from QSController - val stackRounding = MutableStateFlow(StackRounding(roundTop = true, roundBottom = false)) - /** * The height in px of the contents of notification stack. Depending on the number of * notifications, this can exceed the space available on screen to show notifications, at which diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 5a56ca1444dc..f05d01717a44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding @@ -25,6 +27,9 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf /** An interactor which controls the appearance of the NSSL */ @SysUISingleton @@ -32,12 +37,30 @@ class NotificationStackAppearanceInteractor @Inject constructor( private val repository: NotificationStackAppearanceRepository, + shadeInteractor: ShadeInteractor, ) { /** The bounds of the notification stack in the current scene. */ val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow() + /** + * Whether the stack is expanding from GONE-with-HUN to SHADE + * + * TODO(b/296118689): implement this to match legacy QSController logic + */ + private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false) + /** The rounding of the notification stack. */ - val stackRounding: StateFlow<StackRounding> = repository.stackRounding.asStateFlow() + val stackRounding: Flow<StackRounding> = + combine( + shadeInteractor.shadeMode, + isExpandingFromHeadsUp, + ) { shadeMode, isExpandingFromHeadsUp -> + StackRounding( + roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + roundBottom = shadeMode != ShadeMode.Single, + ) + } + .distinctUntilChanged() /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt deleted file mode 100644 index 189c5e03ce07..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.stack.ui.viewbinder - -import android.content.Context -import android.util.TypedValue -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel -import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch - -/** Binds the shared notification container to its view-model. */ -object NotificationStackAppearanceViewBinder { - const val SCRIM_CORNER_RADIUS = 32f - - @JvmStatic - fun bind( - context: Context, - view: SharedNotificationContainer, - viewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - @Main mainImmediateDispatcher: CoroutineDispatcher, - ): DisposableHandle { - return view.repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.stackClipping.collect { (bounds, rounding) -> - val viewLeft = controller.view.left - val viewTop = controller.view.top - val roundRadius = SCRIM_CORNER_RADIUS.dpToPx(context) - controller.setRoundedClippingBounds( - bounds.left.roundToInt() - viewLeft, - bounds.top.roundToInt() - viewTop, - bounds.right.roundToInt() - viewLeft, - bounds.bottom.roundToInt() - viewTop, - if (rounding.roundTop) roundRadius else 0, - if (rounding.roundBottom) roundRadius else 0, - ) - } - } - - launch { - viewModel.contentTop.collect { - controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) - } - } - - launch { - var wasExpanding = false - viewModel.expandFraction.collect { expandFraction -> - val nowExpanding = expandFraction != 0f && expandFraction != 1f - if (nowExpanding && !wasExpanding) { - controller.onExpansionStarted() - } - ambientState.expansionFraction = expandFraction - controller.expandedHeight = expandFraction * controller.view.height - if (!nowExpanding && wasExpanding) { - controller.onExpansionStopped() - } - wasExpanding = nowExpanding - } - } - - launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } - } - } - } - - private fun Float.dpToPx(context: Context): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this, - context.resources.displayMetrics - ) - .roundToInt() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt new file mode 100644 index 000000000000..1a34bb4f02c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Binds the NSSL/Controller/AmbientState to their ViewModel. */ +@SysUISingleton +class NotificationStackViewBinder +@Inject +constructor( + @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val ambientState: AmbientState, + private val view: NotificationStackScrollLayout, + private val controller: NotificationStackScrollLayoutController, + private val viewModel: NotificationStackAppearanceViewModel, + private val configuration: ConfigurationState, +) { + + fun bindWhileAttached(): DisposableHandle { + return view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } + } + + suspend fun bind() = coroutineScope { + launch { + combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) -> + val (bounds, rounding) = clipping + val viewLeft = controller.view.left + val viewTop = controller.view.top + controller.setRoundedClippingBounds( + bounds.left.roundToInt() - viewLeft, + bounds.top.roundToInt() - viewTop, + bounds.right.roundToInt() - viewLeft, + bounds.bottom.roundToInt() - viewTop, + if (rounding.roundTop) clipRadius else 0, + if (rounding.roundBottom) clipRadius else 0, + ) + } + } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + + launch { + var wasExpanding = false + viewModel.expandFraction.collect { expandFraction -> + val nowExpanding = expandFraction != 0f && expandFraction != 1f + if (nowExpanding && !wasExpanding) { + controller.onExpansionStarted() + } + ambientState.expansionFraction = expandFraction + controller.expandedHeight = expandFraction * controller.view.height + if (!nowExpanding && wasExpanding) { + controller.onExpansionStopped() + } + wasExpanding = nowExpanding + } + } + + launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } + } + + private val clipRadius: Flow<Int> + get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 7c76ddbec105..6db6719c76c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -30,6 +31,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import com.android.systemui.util.kotlin.DisposableHandles +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.MutableStateFlow @@ -38,18 +41,23 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ -object SharedNotificationContainerBinder { +@SysUISingleton +class SharedNotificationContainerBinder +@Inject +constructor( + private val sceneContainerFlags: SceneContainerFlags, + private val controller: NotificationStackScrollLayoutController, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, +) { - @JvmStatic fun bind( view: SharedNotificationContainer, viewModel: SharedNotificationContainerViewModel, - sceneContainerFlags: SceneContainerFlags, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { - val disposableHandle = + val disposables = DisposableHandles() + + disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -72,24 +80,6 @@ object SharedNotificationContainerBinder { } } - // Required to capture keyguard media changes and ensure the notification count is correct - val layoutChangeListener = - object : View.OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - viewModel.notificationStackChanged() - } - } - val burnInParams = MutableStateFlow(BurnInParameters()) val viewState = ViewStateAccessor( @@ -100,7 +90,7 @@ object SharedNotificationContainerBinder { * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. */ - val disposableHandleMainImmediate = + disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -167,7 +157,8 @@ object SharedNotificationContainerBinder { } } - controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() } + disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) } view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() @@ -176,16 +167,16 @@ object SharedNotificationContainerBinder { } insets } - view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) } - return object : DisposableHandle { - override fun dispose() { - disposableHandle.dispose() - disposableHandleMainImmediate.dispose() - controller.setOnHeightChangedRunnable(null) - view.setOnApplyWindowInsetsListener(null) - view.removeOnLayoutChangeListener(layoutChangeListener) + // Required to capture keyguard media changes and ensure the notification count is correct + val layoutChangeListener = + View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + viewModel.notificationStackChanged() } - } + view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) } + + return disposables } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index ed44f20868b8..bd83121d9a34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -28,7 +28,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.StackBound import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow /** * ViewModel used by the Notification placeholders inside the scene container to update the @@ -73,7 +72,7 @@ constructor( } /** Corner rounding of the stack */ - val stackRounding: StateFlow<StackRounding> = interactor.stackRounding + val stackRounding: Flow<StackRounding> = interactor.stackRounding /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt new file mode 100644 index 000000000000..de036eaebaa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.DisposableHandle + +/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */ +class DisposableHandles : DisposableHandle { + private val handles = mutableListOf<DisposableHandle>() + + /** Add the provided handles to this collection. */ + fun add(vararg handles: DisposableHandle) { + this.handles.addAll(handles) + } + + /** Same as [add] */ + operator fun plusAssign(handle: DisposableHandle) { + this.handles.add(handle) + } + + /** Same as [add] */ + operator fun plusAssign(handles: Iterable<DisposableHandle>) { + this.handles.addAll(handles) + } + + /** [dispose] the current contents, then [add] the provided [handles] */ + fun replaceAll(vararg handles: DisposableHandle) { + dispose() + add(*handles) + } + + /** Dispose of all added handles and empty this collection. */ + override fun dispose() { + handles.forEach { it.dispose() } + handles.clear() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index 5dd37ae46ee8..66aa572dbc48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -131,7 +131,6 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) - whenever(clockViewModel.clock).thenReturn(clock) currentClock.value = clock } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt index 6ef74194fd85..ba07a849469d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt @@ -19,4 +19,5 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt index 27803b22de29..c06554573bd7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.data.repository.bouncerRepository import com.android.systemui.classifier.domain.interactor.falsingInteractor @@ -29,12 +28,10 @@ import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.bouncerInteractor by Fixture { BouncerInteractor( applicationScope = testScope.backgroundScope, - applicationContext = applicationContext, repository = bouncerRepository, authenticationInteractor = authenticationInteractor, deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, - simBouncerInteractor = simBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt index 8ed9f45bd1ba..02b79af15c05 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt @@ -38,7 +38,7 @@ val Kosmos.simBouncerInteractor by Fixture { telephonyManager = telephonyManager, resources = mainResources, keyguardUpdateMonitor = keyguardUpdateMonitor, - euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager, + euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?, mobileConnectionsRepository = mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt new file mode 100644 index 000000000000..4b6441628500 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.bouncer.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.bouncer.shared.flag.composeBouncerFlags +import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel +import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.bouncerMessageViewModel by + Kosmos.Fixture { + BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = testScope.backgroundScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + selectedUser = userSwitcherViewModel.selectedUser, + clock = systemClock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = deviceEntryFingerprintAuthInteractor, + flags = composeBouncerFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 6d97238ba48b..0f6c7cf13211 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.applicationContext @@ -30,7 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import com.android.systemui.util.mockito.mock -import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.bouncerViewModel by Fixture { BouncerViewModel( @@ -47,7 +49,7 @@ val Kosmos.bouncerViewModel by Fixture { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = bouncerActionButtonInteractor.actionButton, - clock = systemClock, devicePolicyManager = mock(), + bouncerMessageViewModel = bouncerMessageViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt index 546a1e019c6b..5605d1000f4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt @@ -18,10 +18,12 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository val Kosmos.notificationStackAppearanceInteractor by Fixture { NotificationStackAppearanceInteractor( repository = notificationStackAppearanceRepository, + shadeInteractor = shadeInteractor, ) } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e8c05c6d9899..de000bf64c38 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -73,6 +73,7 @@ import android.app.role.RoleManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; +import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -12207,7 +12208,9 @@ public class AudioService extends IAudioService.Stub //========================================================================================== public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { + Objects.requireNonNull(attributionSource); AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback); if (!isPolicyRegisterAllowed(policyConfig, @@ -12228,7 +12231,8 @@ public class AudioService extends IAudioService.Stub } try { AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener, - isFocusPolicy, isTestFocusPolicy, isVolumeController, projection); + isFocusPolicy, isTestFocusPolicy, isVolumeController, projection, + attributionSource); pcb.asBinder().linkToDeath(app, 0/*flags*/); // logging after registration so we have the registration id @@ -13200,6 +13204,7 @@ public class AudioService extends IAudioService.Stub public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient { private static final String TAG = "AudioPolicyProxy"; final IAudioPolicyCallback mPolicyCallback; + final AttributionSource mAttributionSource; final boolean mHasFocusListener; final boolean mIsVolumeController; final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities = @@ -13239,10 +13244,12 @@ public class AudioService extends IAudioService.Stub AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { super(config); setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++)); mPolicyCallback = token; + mAttributionSource = attributionSource; mHasFocusListener = hasFocusListener; mIsVolumeController = isVolumeController; mProjection = projection; @@ -13370,6 +13377,7 @@ public class AudioService extends IAudioService.Stub if (android.media.audiopolicy.Flags.audioMixOwnership()) { for (AudioMix mix : mixes) { setMixRegistration(mix); + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); } int result = mAudioSystem.registerPolicyMixes(mixes, true); @@ -13393,6 +13401,9 @@ public class AudioService extends IAudioService.Stub @AudioSystem.AudioSystemError int connectMixes() { final long identity = Binder.clearCallingIdentity(); try { + for (AudioMix mix : mMixes) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } return mAudioSystem.registerPolicyMixes(mMixes, true); } finally { Binder.restoreCallingIdentity(identity); @@ -13406,6 +13417,9 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(mixesToUpdate); Objects.requireNonNull(updatedMixingRules); + for (AudioMix mix : mixesToUpdate) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } if (mixesToUpdate.length != updatedMixingRules.length) { Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules " + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d0a83a66dfba..cfd64c47718c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1248,7 +1248,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mService.publishLocalService(); IInputMethodManager.Stub service; if (Flags.useZeroJankProxy()) { - service = new ZeroJankProxy(mService.mHandler::post, mService); + service = + new ZeroJankProxy( + mService.mHandler::post, + mService, + () -> { + synchronized (ImfLock.class) { + return mService.isInputShown(); + } + }); } else { service = mService; } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 396192e085e7..136ab42cd0e8 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -46,7 +46,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.util.ExceptionUtils; import android.util.Slog; import android.view.WindowManager; import android.view.inputmethod.CursorAnchorInfo; @@ -77,6 +76,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * A proxy that processes all {@link IInputMethodManager} calls asynchronously. @@ -86,10 +86,12 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private final IInputMethodManager mInner; private final Executor mExecutor; + private final BooleanSupplier mIsInputShown; - ZeroJankProxy(Executor executor, IInputMethodManager inner) { + ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) { mInner = inner; mExecutor = executor; + mIsInputShown = isInputShown; } private void offload(ThrowingRunnable r) { @@ -163,8 +165,19 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { int lastClickTooType, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType, - resultReceiver, reason)); + offload( + () -> { + if (!mInner.showSoftInput( + client, + windowToken, + statsToken, + flags, + lastClickTooType, + resultReceiver, + reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } @@ -173,11 +186,24 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver, - reason)); + offload( + () -> { + if (!mInner.hideSoftInput( + client, windowToken, statsToken, flags, resultReceiver, reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } + private void sendResultReceiverFailure(ResultReceiver resultReceiver) { + resultReceiver.send( + mIsInputShown.getAsBoolean() + ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN, + null); + } + @Override @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() throws RemoteException { diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index a9a82725223d..5b3934ea9b13 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -687,27 +687,20 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private static String toVolumeControlTypeString( @VolumeProvider.ControlType int volumeControlType) { - switch (volumeControlType) { - case VOLUME_CONTROL_FIXED: - return "FIXED"; - case VOLUME_CONTROL_RELATIVE: - return "RELATIVE"; - case VOLUME_CONTROL_ABSOLUTE: - return "ABSOLUTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeControlType); - } + return switch (volumeControlType) { + case VOLUME_CONTROL_FIXED -> "FIXED"; + case VOLUME_CONTROL_RELATIVE -> "RELATIVE"; + case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeControlType); + }; } private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) { - switch (volumeType) { - case PLAYBACK_TYPE_LOCAL: - return "LOCAL"; - case PLAYBACK_TYPE_REMOTE: - return "REMOTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeType); - } + return switch (volumeType) { + case PLAYBACK_TYPE_LOCAL -> "LOCAL"; + case PLAYBACK_TYPE_REMOTE -> "REMOTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeType); + }; } @Override diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 4f3cdbc52259..50ca984dcf57 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -310,6 +310,7 @@ public class PreferencesHelper implements RankingConfig { parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY), parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE), bubblePref); + r.bubblePreference = bubblePref; r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY); r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY); r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); @@ -676,7 +677,7 @@ public class PreferencesHelper implements RankingConfig { * @param bubblePreference whether bubbles are allowed. */ public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { - boolean changed = false; + boolean changed; synchronized (mPackagePreferences) { PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid); changed = p.bubblePreference != bubblePreference; diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index c6bb99eed7ee..20b669b96609 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -18,12 +18,12 @@ package com.android.server.pm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY; import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; @@ -555,12 +555,6 @@ public class LauncherAppsService extends SystemService { return false; } - if (!mRoleManager - .getRoleHoldersAsUser( - RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) - .contains(callingPackage.getPackageName())) { - return false; - } if (mContext.checkPermission( Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL, callingPid, @@ -569,6 +563,13 @@ public class LauncherAppsService extends SystemService { return true; } + if (!mRoleManager + .getRoleHoldersAsUser( + RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) + .contains(callingPackage.getPackageName())) { + return false; + } + // TODO(b/321988638): add option to disable with a flag return mContext.checkPermission( android.Manifest.permission.ACCESS_HIDDEN_PROFILES, diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index cea65b55494d..9f46d0ba7df6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -198,7 +198,9 @@ public class InputMethodManagerServiceWindowGainedFocusTest @Test public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException { - when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false); + // Run blockingly on ServiceThread to avoid that interfering with our stubbing. + mServiceThread.getThreadHandler().runWithScissors( + () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0); assertThat( startInputOrWindowGainedFocus( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index bfc47fdef5cb..cee6cdb06bf5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3962,6 +3962,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + + mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + + ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL); + loadStreamXml(stream, true, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + } + + @Test public void testUpdateNotificationChannel_fixedPermission() { List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0)); when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp index 0e0d212efcf1..8d05a974dc40 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp @@ -26,11 +26,6 @@ android_test { "platform-test-annotations", "platform-test-rules", "truth", - - // beadstead - "Nene", - "Harrier", - "TestApp", ], test_suites: [ "general-tests", diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index 867c0a6e8a02..b66ceba458ac 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -23,20 +23,14 @@ import android.content.pm.PackageManager; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.bedstead.harrier.BedsteadJUnit4; -import com.android.bedstead.harrier.DeviceState; - import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; -@RunWith(BedsteadJUnit4.class) +@RunWith(JUnit4.class) public final class ConcurrentMultiUserTest { - @Rule - public static final DeviceState sDeviceState = new DeviceState(); - @Before public void doBeforeEachTest() { // No op |