diff options
296 files changed, 9139 insertions, 2644 deletions
diff --git a/BAL_OWNERS b/BAL_OWNERS index d56a1d4634df..ec779e76fa3b 100644 --- a/BAL_OWNERS +++ b/BAL_OWNERS @@ -2,4 +2,6 @@ brufino@google.com achim@google.com topjohnwu@google.com lus@google.com +haok@google.com +wnan@google.com diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index dfa72069c28a..ee03e4b2ccd1 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -120,6 +120,7 @@ import android.os.SystemProperties; import android.os.ThreadLocalWorkSource; import android.os.Trace; import android.os.UserHandle; +import android.os.UserManager; import android.os.WorkSource; import android.provider.DeviceConfig; import android.provider.Settings; @@ -1794,7 +1795,8 @@ public class AlarmManagerService extends SystemService { mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms(); - mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms(); + mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms() + && UserManager.supportsMultipleUsers(); if (mStartUserBeforeScheduledAlarms) { mUserWakeupStore = new UserWakeupStore(); mUserWakeupStore.init(); @@ -3015,7 +3017,7 @@ public class AlarmManagerService extends SystemService { mUseFrozenStateToDropListenerAlarms); pw.println(); pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS, - mStartUserBeforeScheduledAlarms); + Flags.startUserBeforeScheduledAlarms()); pw.decreaseIndent(); pw.println(); pw.println(); diff --git a/api/coverage/tools/ExtractFlaggedApis.kt b/api/coverage/tools/ExtractFlaggedApis.kt index bf67187f4bad..0a3ae4f790b0 100644 --- a/api/coverage/tools/ExtractFlaggedApis.kt +++ b/api/coverage/tools/ExtractFlaggedApis.kt @@ -16,9 +16,9 @@ package android.platform.coverage +import com.android.tools.metalava.model.CallableItem import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.Item -import com.android.tools.metalava.model.MethodItem import com.android.tools.metalava.model.text.ApiFile import java.io.File import java.io.FileWriter @@ -40,24 +40,24 @@ fun main(args: Array<String>) { fun extractFlaggedApisFromClass( classItem: ClassItem, - methods: List<MethodItem>, + callables: List<CallableItem>, packageName: String, builder: FlagApiMap.Builder ) { - if (methods.isEmpty()) return + if (callables.isEmpty()) return val classFlag = getClassFlag(classItem) - for (method in methods) { - val methodFlag = getFlagAnnotation(method) ?: classFlag + for (callable in callables) { + val callableFlag = getFlagAnnotation(callable) ?: classFlag val api = JavaMethod.newBuilder() .setPackageName(packageName) .setClassName(classItem.fullName()) - .setMethodName(method.name()) - for (param in method.parameters()) { + .setMethodName(callable.name()) + for (param in callable.parameters()) { api.addParameters(param.type().toTypeString()) } - if (methodFlag != null) { - addFlaggedApi(builder, api, methodFlag) + if (callableFlag != null) { + addFlaggedApi(builder, api, callableFlag) } } } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 26ee4e821ede..c2c7b81871df 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -317,3 +317,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "modify_private_space_secondary_unlock_setup_flow" + namespace: "profile_experiences" + description: "Updates to setting up secondary unlock factor from Settings for the first time" + bug: "332850595" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index 978a8f9200ba..e3dbb2bbbf90 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -747,6 +747,10 @@ public final class StreamConfigurationMap { * {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList}. * </p> * + * <p>This function returns an empty array if + * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO} + * is not supported.</p> + * * @return an array of supported high speed video recording sizes * @see #getHighSpeedVideoFpsRangesFor(Size) * @see CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO @@ -836,6 +840,10 @@ public final class StreamConfigurationMap { * supported for the same recording rate.</li> * </p> * + * <p>This function returns an empty array if + * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO} + * is not supported.</p> + * * @return an array of supported high speed video recording FPS ranges The upper bound of * returned ranges is guaranteed to be larger or equal to 120. * @see #getHighSpeedVideoSizesFor diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java index 6a96a54d93ba..c09106206c25 100644 --- a/core/java/android/hardware/display/BrightnessInfo.java +++ b/core/java/android/hardware/display/BrightnessInfo.java @@ -60,8 +60,7 @@ public final class BrightnessInfo implements Parcelable { @IntDef(prefix = {"BRIGHTNESS_MAX_REASON_"}, value = { BRIGHTNESS_MAX_REASON_NONE, BRIGHTNESS_MAX_REASON_THERMAL, - BRIGHTNESS_MAX_REASON_POWER_IC, - BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE + BRIGHTNESS_MAX_REASON_POWER_IC }) @Retention(RetentionPolicy.SOURCE) public @interface BrightnessMaxReason {} @@ -158,8 +157,6 @@ public final class BrightnessInfo implements Parcelable { return "thermal"; case BRIGHTNESS_MAX_REASON_POWER_IC: return "power IC"; - case BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE: - return "wear bedtime"; } return "invalid"; } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 0321e1dfee78..97f6899ff141 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1669,6 +1669,46 @@ public final class DisplayManager { } /** + * Gets the mapping between the doze brightness sensor values and brightness values. The doze + * brightness sensor is a light sensor used to determine the brightness while the device is + * dozing. Light sensor values are typically integers in the rage of 0-4. The returned values + * are between {@link PowerManager#BRIGHTNESS_MIN} and {@link PowerManager#BRIGHTNESS_MAX}, or + * -1 meaning that the current brightness should be kept. + * <p> + * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} + * permission. + * </p> + * + * @param displayId The ID of the display + * + * @hide + */ + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @Nullable + public float[] getDozeBrightnessSensorValueToBrightness(int displayId) { + return mGlobal.getDozeBrightnessSensorValueToBrightness(displayId); + } + + /** + * Gets the default doze brightness. + * The returned values are between {@link PowerManager#BRIGHTNESS_MIN} and + * {@link PowerManager#BRIGHTNESS_MAX}. + * <p> + * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} + * permission. + * </p> + * + * @param displayId The ID of the display + * + * @hide + */ + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @FloatRange(from = 0f, to = 1f) + public float getDefaultDozeBrightness(int displayId) { + return mGlobal.getDefaultDozeBrightness(displayId); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index e9cd37aedec9..cae33d05b6ed 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -21,6 +21,7 @@ import static android.hardware.display.DisplayManager.EventsMask; import static android.view.Display.HdrCapabilities.HdrType; import android.Manifest; +import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1226,6 +1227,32 @@ public final class DisplayManagerGlobal { } } + /** + * @see DisplayManager#getDozeBrightnessSensorValueToBrightness + */ + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @Nullable + public float[] getDozeBrightnessSensorValueToBrightness(int displayId) { + try { + return mDm.getDozeBrightnessSensorValueToBrightness(displayId); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * @see DisplayManager#getDefaultDozeBrightness + */ + @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @FloatRange(from = 0f, to = 1f) + public float getDefaultDozeBrightness(int displayId) { + try { + return mDm.getDefaultDozeBrightness(displayId); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, @DisplayEvent int event) { diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index 77277ee7bb08..f3c21e9f7a43 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -246,4 +246,12 @@ interface IDisplayManager { // Restricts display modes to specified modeIds. @EnforcePermission("RESTRICT_DISPLAY_MODES") void requestDisplayModes(in IBinder token, int displayId, in @nullable int[] modeIds); + + // Get the mapping between the doze brightness sensor values and brightness values + @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS") + float[] getDozeBrightnessSensorValueToBrightness(int displayId); + + // Get the default doze brightness + @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS") + float getDefaultDozeBrightness(int displayId); } diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index e2d215ebfed4..4bc5bd2427ea 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -29,6 +29,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSession; import android.window.WindowProviderService; @@ -186,6 +187,10 @@ public abstract class AbstractInputMethodService extends WindowProviderService if (callback != null) { callback.finishedEvent(seq, handled); } + if (Flags.imeSwitcherRevamp() && !handled && event.getAction() == KeyEvent.ACTION_DOWN + && event.getUnicodeChar() > 0 && mInputMethodServiceInternal != null) { + mInputMethodServiceInternal.notifyUserActionIfNecessary(); + } } /** diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 943b04f517f3..855c3098c9e7 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -16,7 +16,6 @@ package android.inputmethodservice; -import static android.view.inputmethod.Flags.predictiveBackIme; import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VIEW_STARTED; import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VISIBILITY; import static android.inputmethodservice.InputMethodServiceProto.CONFIGURATION; @@ -57,6 +56,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; import static android.view.inputmethod.Flags.ctrlShiftShortcut; +import static android.view.inputmethod.Flags.predictiveBackIme; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -4341,6 +4341,16 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Called when the IME switch button was clicked from the client. This will show the input + * method picker dialog. + * + * @hide + */ + final void onImeSwitchButtonClickFromClient() { + mPrivOps.onImeSwitchButtonClickFromClient(getDisplayId()); + } + + /** * Used to inject custom {@link InputMethodServiceInternal}. * * @return the {@link InputMethodServiceInternal} to be used. diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index de67e06ddda7..3ce67b0e0ec1 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -42,6 +42,7 @@ import android.view.WindowInsets; import android.view.WindowInsetsController.Appearance; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import com.android.internal.inputmethod.InputMethodNavButtonFlags; @@ -145,7 +146,8 @@ final class NavigationBarController { return mImpl.toDebugString(); } - private static final class Impl implements Callback, Window.DecorCallback { + private static final class Impl implements Callback, Window.DecorCallback, + NavigationBarView.ButtonClickListener { private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700; // Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE @@ -241,6 +243,7 @@ final class NavigationBarController { ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); navigationBarView.setNavigationIconHints(hints); + navigationBarView.prepareNavButtons(this); } } else { mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams( @@ -592,6 +595,17 @@ final class NavigationBarController { return drawLegacyNavigationBarBackground; } + @Override + public void onImeSwitchButtonClick(View v) { + mService.onImeSwitchButtonClickFromClient(); + } + + @Override + public boolean onImeSwitchButtonLongClick(View v) { + v.getContext().getSystemService(InputMethodManager.class).showInputMethodPicker(); + return true; + } + /** * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead. */ diff --git a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java index f423672c5490..540243c4fe92 100644 --- a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java +++ b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java @@ -41,6 +41,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputConnection; import android.widget.ImageView; @@ -58,12 +59,30 @@ public class KeyButtonView extends ImageView implements ButtonInterface { private int mTouchDownY; private AudioManager mAudioManager; private boolean mGestureAborted; + /** + * Whether the long click action has been invoked. The short click action is invoked on the up + * event while a long click is invoked as soon as the long press duration is reached, so a long + * click could be performed before the short click is checked, in which case the short click's + * action should not be invoked. + * + * @see View#mHasPerformedLongPress + */ + private boolean mLongClicked; private OnClickListener mOnClickListener; private final KeyButtonRipple mRipple; private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private float mDarkIntensity; private boolean mHasOvalBg = false; + /** Runnable for checking whether the long click action should be performed. */ + private final Runnable mCheckLongPress = new Runnable() { + public void run() { + if (isPressed() && performLongClick()) { + mLongClicked = true; + } + } + }; + public KeyButtonView(Context context, AttributeSet attrs) { super(context, attrs); @@ -159,6 +178,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { switch (action) { case MotionEvent.ACTION_DOWN: mDownTime = SystemClock.uptimeMillis(); + mLongClicked = false; setPressed(true); // Use raw X and Y to detect gestures in case a parent changes the x and y values @@ -173,6 +193,10 @@ public class KeyButtonView extends ImageView implements ButtonInterface { if (!showSwipeUI) { playSoundEffect(SoundEffectConstants.CLICK); } + if (Flags.imeSwitcherRevamp() && isLongClickable()) { + removeCallbacks(mCheckLongPress); + postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); + } break; case MotionEvent.ACTION_MOVE: x = (int) ev.getRawX(); @@ -183,6 +207,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface { // When quick step is enabled, prevent animating the ripple triggered by // setPressed and decide to run it on touch up setPressed(false); + if (isLongClickable()) { + removeCallbacks(mCheckLongPress); + } } break; case MotionEvent.ACTION_CANCEL: @@ -190,9 +217,12 @@ public class KeyButtonView extends ImageView implements ButtonInterface { if (mCode != KEYCODE_UNKNOWN) { sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); } + if (isLongClickable()) { + removeCallbacks(mCheckLongPress); + } break; case MotionEvent.ACTION_UP: - final boolean doIt = isPressed(); + final boolean doIt = isPressed() && !mLongClicked; setPressed(false); final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150; if (showSwipeUI) { @@ -201,7 +231,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); playSoundEffect(SoundEffectConstants.CLICK); } - } else if (doHapticFeedback) { + } else if (doHapticFeedback && !mLongClicked) { // Always send a release ourselves because it doesn't seem to be sent elsewhere // and it feels weird to sometimes get a release haptic and other times not. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE); @@ -221,6 +251,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); } } + if (isLongClickable()) { + removeCallbacks(mCheckLongPress); + } break; } diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index e28f34528f42..a3beaf427226 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java @@ -26,6 +26,7 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.DrawableRes; import android.annotation.FloatRange; +import android.annotation.NonNull; import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; @@ -39,6 +40,7 @@ import android.view.Surface; import android.view.View; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; @@ -79,6 +81,28 @@ public final class NavigationBarView extends FrameLayout { private NavigationBarInflaterView mNavigationInflaterView; + /** + * Interface definition for callbacks to be invoked when navigation bar buttons are clicked. + */ + public interface ButtonClickListener { + + /** + * Called when the IME switch button is clicked. + * + * @param v The view that was clicked. + */ + void onImeSwitchButtonClick(View v); + + /** + * Called when the IME switch button has been clicked and held. + * + * @param v The view that was clicked and held. + * + * @return true if the callback consumed the long click, false otherwise. + */ + boolean onImeSwitchButtonLongClick(View v); + } + public NavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); @@ -98,13 +122,27 @@ public final class NavigationBarView extends FrameLayout { new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle)); mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this); + } + /** + * Prepares the navigation bar buttons to be used and sets the on click listeners. + * + * @param listener The listener used to handle the clicks on the navigation bar buttons. + */ + public void prepareNavButtons(@NonNull ButtonClickListener listener) { getBackButton().setLongClickable(false); - final ButtonDispatcher imeSwitchButton = getImeSwitchButton(); - imeSwitchButton.setLongClickable(false); - imeSwitchButton.setOnClickListener(view -> view.getContext() - .getSystemService(InputMethodManager.class).showInputMethodPicker()); + if (Flags.imeSwitcherRevamp()) { + final var imeSwitchButton = getImeSwitchButton(); + imeSwitchButton.setLongClickable(true); + imeSwitchButton.setOnClickListener(listener::onImeSwitchButtonClick); + imeSwitchButton.setOnLongClickListener(listener::onImeSwitchButtonLongClick); + } else { + final ButtonDispatcher imeSwitchButton = getImeSwitchButton(); + imeSwitchButton.setLongClickable(false); + imeSwitchButton.setOnClickListener(view -> view.getContext() + .getSystemService(InputMethodManager.class).showInputMethodPicker()); + } } @Override diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index 8b1577cb4b1c..97993b609fda 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -41,5 +41,5 @@ interface IVibratorManagerService { // There is no order guarantee with respect to the two-way APIs above like // vibrate/isVibrating/cancel. oneway void performHapticFeedback(int uid, int deviceId, String opPkg, int constant, - boolean always, String reason, boolean fromIme); + String reason, int flags, int privFlags); } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 406a1a6795ab..026013c34e30 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -564,8 +564,7 @@ public final class PowerManager { BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM, BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM, BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT, - BRIGHTNESS_CONSTRAINT_TYPE_DIM, - BRIGHTNESS_CONSTRAINT_TYPE_DOZE + BRIGHTNESS_CONSTRAINT_TYPE_DIM }) @Retention(RetentionPolicy.SOURCE) public @interface BrightnessConstraint{} @@ -594,12 +593,6 @@ public final class PowerManager { public static final int BRIGHTNESS_CONSTRAINT_TYPE_DIM = 3; /** - * Brightness constraint type: minimum allowed value. - * @hide - */ - public static final int BRIGHTNESS_CONSTRAINT_TYPE_DOZE = 4; - - /** * @hide */ @IntDef(prefix = { "WAKE_REASON_" }, value = { diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 2a62c24a86e1..5339d7331426 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -206,13 +206,12 @@ public class SystemVibrator extends Vibrator { } @Override - public void performHapticFeedback( - int constant, boolean always, String reason, boolean fromIme) { + public void performHapticFeedback(int constant, String reason, int flags, int privFlags) { if (mVibratorManager == null) { Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager."); return; } - mVibratorManager.performHapticFeedback(constant, always, reason, fromIme); + mVibratorManager.performHapticFeedback(constant, reason, flags, privFlags); } @Override diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index c80bcac2624f..a9846ba7e264 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -147,15 +147,14 @@ public class SystemVibratorManager extends VibratorManager { } @Override - public void performHapticFeedback(int constant, boolean always, String reason, - boolean fromIme) { + public void performHapticFeedback(int constant, String reason, int flags, int privFlags) { if (mService == null) { Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service."); return; } try { - mService.performHapticFeedback( - mUid, mContext.getDeviceId(), mPackageName, constant, always, reason, fromIme); + mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant, + reason, flags, privFlags); } catch (RemoteException e) { Log.w(TAG, "Failed to perform haptic feedback.", e); } @@ -245,9 +244,8 @@ public class SystemVibratorManager extends VibratorManager { } @Override - public void performHapticFeedback(int effectId, boolean always, String reason, - boolean fromIme) { - SystemVibratorManager.this.performHapticFeedback(effectId, always, reason, fromIme); + public void performHapticFeedback(int effectId, String reason, int flags, int privFlags) { + SystemVibratorManager.this.performHapticFeedback(effectId, reason, flags, privFlags); } @Override diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 8af371c74880..71c83f20741d 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -33,6 +33,7 @@ import android.media.AudioAttributes; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibratorFrequencyProfile; import android.util.Log; +import android.view.HapticFeedbackConstants; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -519,17 +520,15 @@ public abstract class Vibrator { * * @param constant the ID for the haptic feedback. This should be one of the constants defined * in {@link HapticFeedbackConstants}. - * @param always {@code true} if the haptic feedback should be played regardless of the user - * vibration intensity settings applicable to the corresponding vibration. - * {@code false} if the vibration for the haptic feedback should respect the applicable - * vibration intensity settings. * @param reason the reason for this haptic feedback. - * @param fromIme the haptic feedback is performed from an IME. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. * * @hide */ - public void performHapticFeedback(int constant, boolean always, String reason, - boolean fromIme) { + public void performHapticFeedback(int constant, String reason, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { Log.w(TAG, "performHapticFeedback is not supported"); } diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java index 513c4bd7ec0c..2c7a852cf29f 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -23,6 +23,7 @@ import android.annotation.SystemService; import android.app.ActivityThread; import android.content.Context; import android.util.Log; +import android.view.HapticFeedbackConstants; /** * Provides access to all vibrators from the device, as well as the ability to run them @@ -142,15 +143,14 @@ public abstract class VibratorManager { * * @param constant the ID of the requested haptic feedback. Should be one of the constants * defined in {@link HapticFeedbackConstants}. - * @param always {@code true} if the haptic feedback should be played regardless of the user - * vibration intensity settings applicable to the corresponding vibration. - * {@code false} otherwise. * @param reason the reason for this haptic feedback. - * @param fromIme the haptic feedback is performed from an IME. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. * @hide */ - public void performHapticFeedback(int constant, boolean always, String reason, - boolean fromIme) { + public void performHapticFeedback(int constant, String reason, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { Log.w(TAG, "performHapticFeedback is not supported"); } diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index c73a422d931b..ad2f59db46ff 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -46,6 +46,7 @@ flag { flag { namespace: "haptics" name: "vibration_xml_apis" + is_exported: true description: "Enabled System APIs for vibration effect XML parser and serializer" bug: "347273158" metadata { diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 69228cafa34b..1fe06d474803 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -16,11 +16,30 @@ package android.view; +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Constants to be used to perform haptic feedback effects via * {@link View#performHapticFeedback(int)} */ public class HapticFeedbackConstants { + /** @hide **/ + @IntDef(flag = true, prefix = "FLAG_", value = { + FLAG_IGNORE_VIEW_SETTING, + FLAG_IGNORE_GLOBAL_SETTING, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags {} + + /** @hide **/ + @IntDef(flag = true, prefix = "PRIVATE_FLAG_", value = { + PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PrivateFlags {} private HapticFeedbackConstants() {} @@ -258,4 +277,14 @@ public class HapticFeedbackConstants { */ @Deprecated public static final int FLAG_IGNORE_GLOBAL_SETTING = 0x0002; + + /** + * Flag for {@link android.os.Vibrator#performHapticFeedback(int, boolean, String, int, int)} or + * {@link ViewRootImpl#performHapticFeedback(int, boolean, int, int)}: Perform the haptic + * feedback with the input method vibration settings, e.g. applying the keyboard vibration + * user settings to the KEYBOARD_* constants. + * + * @hide + */ + public static final int PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS = 0x0001; } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 070d33bb9be6..14407ca32388 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -140,13 +140,13 @@ interface IWindowSession { int seqId); @UnsupportedAppUsage - boolean performHapticFeedback(int effectId, boolean always, boolean fromIme); + boolean performHapticFeedback(int effectId, int flags, int privFlags); /** * Called by attached views to perform predefined haptic feedback without requiring VIBRATE * permission. */ - oneway void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme); + oneway void performHapticFeedbackAsync(int effectId, int flags, int privFlags); /** * Initiate the drag operation itself diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 88d7a834ff53..2edbcc2592cc 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -28636,20 +28636,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; } - final boolean always = (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0; - boolean fromIme = false; - if (mAttachInfo.mViewRootImpl != null) { - fromIme = mAttachInfo.mViewRootImpl.mWindowAttributes.type == TYPE_INPUT_METHOD; + int privFlags = 0; + if (mAttachInfo.mViewRootImpl != null + && mAttachInfo.mViewRootImpl.mWindowAttributes.type == TYPE_INPUT_METHOD) { + privFlags = HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS; } if (Flags.useVibratorHapticFeedback()) { if (!mAttachInfo.canPerformHapticFeedback()) { return false; } - getSystemVibrator().performHapticFeedback( - feedbackConstant, always, "View#performHapticFeedback", fromIme); + getSystemVibrator().performHapticFeedback(feedbackConstant, + "View#performHapticFeedback", flags, privFlags); return true; } - return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, always, fromIme); + return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, flags, privFlags); } private Vibrator getSystemVibrator() { @@ -31684,7 +31684,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, interface Callbacks { void playSoundEffect(int effectId); - boolean performHapticFeedback(int effectId, boolean always, boolean fromIme); + + boolean performHapticFeedback(int effectId, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags); } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e5b17c8d1001..596726f83c15 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -9666,18 +9666,18 @@ public final class ViewRootImpl implements ViewParent, * {@inheritDoc} */ @Override - public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) { + public boolean performHapticFeedback(int effectId, int flags, int privFlags) { if ((mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) { return false; } try { if (USE_ASYNC_PERFORM_HAPTIC_FEEDBACK) { - mWindowSession.performHapticFeedbackAsync(effectId, always, fromIme); + mWindowSession.performHapticFeedbackAsync(effectId, flags, privFlags); return true; } else { // Original blocking binder call path. - return mWindowSession.performHapticFeedback(effectId, always, fromIme); + return mWindowSession.performHapticFeedback(effectId, flags, privFlags); } } catch (RemoteException e) { return false; diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 55f22a631c3b..78718586329e 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -503,13 +503,13 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) { + public boolean performHapticFeedback(int effectId, int flags, int privFlags) { return false; } @Override - public void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme) { - performHapticFeedback(effectId, always, fromIme); + public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) { + performHapticFeedback(effectId, flags, privFlags); } @Override diff --git a/core/java/android/view/accessibility/a11ychecker/Android.bp b/core/java/android/view/accessibility/a11ychecker/Android.bp deleted file mode 100644 index e5a577c16421..000000000000 --- a/core/java/android/view/accessibility/a11ychecker/Android.bp +++ /dev/null @@ -1,7 +0,0 @@ -java_library_static { - name: "A11yChecker", - srcs: [ - "*.java", - ], - visibility: ["//visibility:public"], -} diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index e7a2fb92a382..07a97948e7fd 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -465,6 +465,20 @@ final class IInputMethodManagerGlobalInvoker { } @AnyThread + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + static void onImeSwitchButtonClickFromSystem(int displayId) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.onImeSwitchButtonClickFromSystem(displayId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread @Nullable @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0c63e583f326..dbbfff04f436 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -4354,6 +4354,19 @@ public final class InputMethodManager { } /** + * Called when the IME switch button was clicked from the system. This will show the input + * method picker dialog. + * + * @param displayId The ID of the display where the input method picker dialog should be shown. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + public void onImeSwitchButtonClickFromSystem(int displayId) { + IInputMethodManagerGlobalInvoker.onImeSwitchButtonClickFromSystem(displayId); + } + + /** * A test API for CTS to check whether there are any pending IME visibility requests. * * @return {@code true} iff there are pending IME visibility requests. diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java index 739cf0eeca0c..0d5e37e19916 100644 --- a/core/java/android/window/WindowMetricsController.java +++ b/core/java/android/window/WindowMetricsController.java @@ -75,8 +75,8 @@ public final class WindowMetricsController { /** * The core implementation to obtain {@link WindowMetrics} * - * @param isMaximum {@code true} to obtain {@link WindowManager#getCurrentWindowMetrics()}. - * {@code false} to obtain {@link WindowManager#getMaximumWindowMetrics()}. + * @param isMaximum {@code false} to obtain {@link WindowManager#getCurrentWindowMetrics()}. + * {@code true} to obtain {@link WindowManager#getMaximumWindowMetrics()}. */ private WindowMetrics getWindowMetricsInternal(boolean isMaximum) { final Rect bounds; diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index cc880e182b2b..48fb2b3ab129 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -91,6 +91,13 @@ flag { } flag { + name: "camera_compat_fullscreen_pick_same_task_activity" + namespace: "large_screen_experiences_app_compat" + description: "Limit undo of camera compat treatment to the same task that started the treatment." + bug: "350495350" +} + +flag { name: "app_compat_refactoring" namespace: "large_screen_experiences_app_compat" description: "Whether the changes about app compat refactoring are enabled./n" diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 1362f7bf9619..3f1c06ac7e10 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -176,3 +176,10 @@ flag { description: "Enables desktop windowing app handle education" bug: "348208342" } + +flag { + name: "enable_compat_ui_visibility_status" + namespace: "lse_desktop_experience" + description: "Enables the tracking of the status for compat ui elements." + bug: "350953004" +} diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl index 63623c767694..ac4c066a992f 100644 --- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl +++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl @@ -43,6 +43,7 @@ oneway interface IInputMethodPrivilegedOperations { void switchToPreviousInputMethod(in AndroidFuture future /* T=Boolean */); void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */); void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */); + void onImeSwitchButtonClickFromClient(int displayId); void notifyUserActionAsync(); void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, in ImeTracker.Token statsToken); diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java index 72c41bed4436..2daf0fd1f61c 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java +++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java @@ -378,6 +378,22 @@ public final class InputMethodPrivilegedOperations { } /** + * Calls {@link IInputMethodPrivilegedOperations#onImeSwitchButtonClickFromClient(int)} + */ + @AnyThread + public void onImeSwitchButtonClickFromClient(int displayId) { + final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); + if (ops == null) { + return; + } + try { + ops.onImeSwitchButtonClickFromClient(displayId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()} */ @AnyThread diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 2b3ffeb2e619..cba27ce3fe65 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -135,6 +135,19 @@ interface IInputMethodManager { + "android.Manifest.permission.TEST_INPUT_METHOD)") boolean isInputMethodPickerShownForTest(); + /** + * Called when the IME switch button was clicked from the system. Depending on the number of + * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input + * method picker dialog. + * + * @param displayId The ID of the display where the input method picker dialog should be shown. + * @param userId The ID of the user that triggered the click. + */ + @EnforcePermission("WRITE_SECURE_SETTINGS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.WRITE_SECURE_SETTINGS)") + oneway void onImeSwitchButtonClickFromSystem(int displayId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") @nullable InputMethodSubtype getCurrentInputMethodSubtype(int userId); diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp index d11166fd6187..e874163943b6 100644 --- a/core/jni/android_view_InputChannel.cpp +++ b/core/jni/android_view_InputChannel.cpp @@ -16,18 +16,22 @@ #define LOG_TAG "InputChannel-JNI" -#include "android-base/stringprintf.h" -#include <nativehelper/JNIHelp.h> -#include "nativehelper/scoped_utf_chars.h" +#include "android_view_InputChannel.h" + #include <android_runtime/AndroidRuntime.h> #include <binder/Parcel.h> -#include <utils/Log.h> +#include <com_android_input_flags.h> #include <input/InputTransport.h> -#include "android_view_InputChannel.h" +#include <nativehelper/JNIHelp.h> +#include <utils/Log.h> + +#include "android-base/stringprintf.h" #include "android_os_Parcel.h" #include "android_util_Binder.h" - #include "core_jni_helpers.h" +#include "nativehelper/scoped_utf_chars.h" + +namespace input_flags = com::android::input::flags; namespace android { @@ -69,6 +73,9 @@ NativeInputChannel::~NativeInputChannel() { } void NativeInputChannel::setDisposeCallback(InputChannelObjDisposeCallback callback, void* data) { + if (input_flags::remove_input_channel_from_windowstate()) { + return; + } mDisposeCallback = callback; mDisposeData = data; } diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 59d18b8535f5..30c926c57693 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -104,6 +104,7 @@ extern int register_android_view_KeyCharacterMap(JNIEnv* env); extern int register_android_view_KeyEvent(JNIEnv* env); extern int register_android_view_InputDevice(JNIEnv* env); extern int register_android_view_MotionEvent(JNIEnv* env); +extern int register_android_view_Surface(JNIEnv* env); extern int register_android_view_ThreadedRenderer(JNIEnv* env); extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); extern int register_android_view_VelocityTracker(JNIEnv* env); @@ -151,6 +152,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.view.KeyEvent", REG_JNI(register_android_view_KeyEvent)}, {"android.view.InputDevice", REG_JNI(register_android_view_InputDevice)}, {"android.view.MotionEvent", REG_JNI(register_android_view_MotionEvent)}, + {"android.view.Surface", REG_JNI(register_android_view_Surface)}, {"android.view.VelocityTracker", REG_JNI(register_android_view_VelocityTracker)}, {"com.android.internal.util.VirtualRefBasePtr", REG_JNI(register_com_android_internal_util_VirtualRefBasePtr)}, diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index d2771691c50c..41696dfa782e 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -63,7 +63,6 @@ android_test { "-c fa", ], static_libs: [ - "A11yChecker", "collector-device-lib-platform", "frameworks-base-testutils", "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 033ac7cec1bc..c5b75ff50da7 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -17,6 +17,7 @@ package android.view; import static android.util.SequenceUtils.getInitSeq; +import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING; import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; @@ -494,8 +495,8 @@ public class ViewRootImplTest { 0, displayInfo, new DisplayAdjustments()); ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display); - boolean result = viewRootImpl.performHapticFeedback( - HapticFeedbackConstants.CONTEXT_CLICK, true, false /* fromIme */); + boolean result = viewRootImpl.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK, + FLAG_IGNORE_GLOBAL_SETTING, 0 /* privFlags */); assertThat(result).isFalse(); } diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS b/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS deleted file mode 100644 index 872a1804555b..000000000000 --- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS +++ /dev/null @@ -1,5 +0,0 @@ -# Android Accessibility Framework owners -include /core/java/android/view/accessibility/a11ychecker/OWNERS -include /services/accessibility/OWNERS - -yaraabdullatif@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index 196f89d5794e..df80946a99aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -107,4 +107,24 @@ public interface BackAnimation { * @param pilferCallback the callback to pilfer pointers. */ void setPilferPointerCallback(Runnable pilferCallback); + + /** + * Set a callback to requestTopUi. + * @param topUiRequest the callback to requestTopUi. + */ + void setTopUiRequestCallback(TopUiRequest topUiRequest); + + /** + * Callback to request SysUi to call + * {@link android.app.IActivityManager#setHasTopUi(boolean)}. + */ + interface TopUiRequest { + + /** + * Request {@link android.app.IActivityManager#setHasTopUi(boolean)} to be called. + * @param requestTopUi whether topUi should be requested or not + * @param tag tag of the request-source + */ + void requestTopUi(boolean requestTopUi, String tag); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 8467e972526e..bb239adfe73f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -179,6 +179,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @BackNavigationInfo.BackTargetType private int mPreviousNavigationType; private Runnable mPilferPointerCallback; + private BackAnimation.TopUiRequest mRequestTopUiCallback; public BackAnimationController( @NonNull ShellInit shellInit, @@ -357,6 +358,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPilferPointerCallback = callback; }); } + + @Override + public void setTopUiRequestCallback(TopUiRequest topUiRequest) { + mShellExecutor.execute(() -> mRequestTopUiCallback = topUiRequest); + } } private static class IBackAnimationImpl extends IBackAnimation.Stub @@ -557,6 +563,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mShellBackAnimationRegistry.startGesture(backType)) { mActiveCallback = null; } + requestTopUi(true, backType); tryPilferPointers(); } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); @@ -906,6 +913,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); mBackNavigationInfo = null; + requestTopUi(false, mPreviousNavigationType); } } @@ -969,6 +977,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } + private void requestTopUi(boolean hasTopUi, int backType) { + if (mRequestTopUiCallback != null && (backType == BackNavigationInfo.TYPE_CROSS_TASK + || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)) { + mRequestTopUiCallback.requestTopUi(hasTopUi, TAG); + } + } + /** * Validate animation targets. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 4f04c5c28412..4e0c82b9628f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -61,8 +61,7 @@ abstract class CrossActivityBackAnimation( private val context: Context, private val background: BackAnimationBackground, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - protected val transaction: SurfaceControl.Transaction, - private val choreographer: Choreographer + protected val transaction: SurfaceControl.Transaction ) : ShellBackAnimation() { protected val startClosingRect = RectF() @@ -269,7 +268,9 @@ abstract class CrossActivityBackAnimation( .setSpring(postCommitFlingSpring) flingAnimation.start() // do an animation-frame immediately to prevent idle frame - flingAnimation.doAnimationFrame(choreographer.lastFrameTimeNanos / TimeUtils.NANOS_PER_MS) + flingAnimation.doAnimationFrame( + Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS + ) val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration()) @@ -362,7 +363,7 @@ abstract class CrossActivityBackAnimation( } protected fun applyTransaction() { - transaction.setFrameTimelineVsync(choreographer.vsyncId) + transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId) transaction.apply() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index 103a65422504..e2b0513c951f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -52,7 +52,6 @@ import com.android.internal.policy.SystemBarUtils; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.shared.annotations.ShellMainThread; import javax.inject.Inject; @@ -69,7 +68,6 @@ import javax.inject.Inject; * IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back navigation to * launcher starts. */ -@ShellMainThread public class CrossTaskBackAnimation extends ShellBackAnimation { private static final int BACKGROUNDCOLOR = 0x43433A; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt index e266e2cd7eea..b02f97bf7784 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt @@ -19,7 +19,6 @@ import android.content.Context import android.graphics.Rect import android.graphics.RectF import android.util.MathUtils -import android.view.Choreographer import android.view.SurfaceControl import android.view.animation.Animation import android.view.animation.Transformation @@ -31,27 +30,23 @@ import com.android.internal.policy.TransitionAnimation import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup -import com.android.wm.shell.shared.annotations.ShellMainThread import javax.inject.Inject import kotlin.math.max import kotlin.math.min /** Class that handles customized predictive cross activity back animations. */ -@ShellMainThread class CustomCrossActivityBackAnimation( context: Context, background: BackAnimationBackground, rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, transaction: SurfaceControl.Transaction, - choreographer: Choreographer, private val customAnimationLoader: CustomAnimationLoader ) : CrossActivityBackAnimation( context, background, rootTaskDisplayAreaOrganizer, - transaction, - choreographer + transaction ) { private var enterAnimation: Animation? = null @@ -70,7 +65,6 @@ class CustomCrossActivityBackAnimation( background, rootTaskDisplayAreaOrganizer, SurfaceControl.Transaction(), - Choreographer.getInstance(), CustomAnimationLoader( TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation") ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt index 3b5eb3613d2a..c747e1e98956 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt @@ -16,18 +16,15 @@ package com.android.wm.shell.back import android.content.Context -import android.view.Choreographer import android.view.SurfaceControl import android.window.BackEvent import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.animation.Interpolators -import com.android.wm.shell.shared.annotations.ShellMainThread import javax.inject.Inject import kotlin.math.max /** Class that defines cross-activity animation. */ -@ShellMainThread class DefaultCrossActivityBackAnimation @Inject constructor( @@ -39,8 +36,7 @@ constructor( context, background, rootTaskDisplayAreaOrganizer, - SurfaceControl.Transaction(), - Choreographer.getInstance() + SurfaceControl.Transaction() ) { private val postCommitInterpolator = Interpolators.EMPHASIZED diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt new file mode 100644 index 000000000000..cb54d89a5714 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.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.wm.shell.compatui.api + +/** + * Abstraction for the repository of all the available CompatUISpec + */ +interface CompatUIRepository { + /** + * Adds a {@link CompatUISpec} to the repository + * @throws IllegalStateException in case of illegal spec + */ + fun addSpec(spec: CompatUISpec) + + /** + * Iterates on the list of available {@link CompatUISpec} invoking + * fn for each of them. + */ + fun iterateOn(fn: (CompatUISpec) -> Unit) + + /** + * Returns the {@link CompatUISpec} for a given key + */ + fun findSpec(name: String): CompatUISpec? +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt new file mode 100644 index 000000000000..24c2c8c2aedf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt @@ -0,0 +1,26 @@ +/* + * 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.wm.shell.compatui.api + +/** + * Describes each compat ui component to the framework. + */ +data class CompatUISpec( + // Unique name for the component. It's used for debug and for generating the + // unique component identifier in the system. + val name: String +)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt index a181eafada7d..8408ea6ebc31 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt @@ -19,12 +19,15 @@ package com.android.wm.shell.compatui.impl import com.android.wm.shell.compatui.api.CompatUIEvent import com.android.wm.shell.compatui.api.CompatUIHandler import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUIRepository import java.util.function.Consumer /** * Default implementation of {@link CompatUIHandler} to handle CompatUI components */ -class DefaultCompatUIHandler : CompatUIHandler { +class DefaultCompatUIHandler( + private val compatUIRepository: CompatUIRepository +) : CompatUIHandler { private var compatUIEventSender: Consumer<CompatUIEvent>? = null override fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt new file mode 100644 index 000000000000..10d9425c85ea --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.wm.shell.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Default {@link CompatUIRepository} implementation + */ +class DefaultCompatUIRepository : CompatUIRepository { + + private val allSpecs = mutableMapOf<String, CompatUISpec>() + + override fun addSpec(spec: CompatUISpec) { + if (allSpecs[spec.name] != null) { + throw IllegalStateException("Spec with id:${spec.name} already present") + } + allSpecs[spec.name] = spec + } + + override fun iterateOn(fn: (CompatUISpec) -> Unit) = + allSpecs.values.forEach(fn) + + override fun findSpec(name: String): CompatUISpec? = + allSpecs[name] +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 9bdc0b2b55b4..4b548cbe77cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -72,7 +72,9 @@ import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; import com.android.wm.shell.compatui.api.CompatUIHandler; +import com.android.wm.shell.compatui.api.CompatUIRepository; import com.android.wm.shell.compatui.impl.DefaultCompatUIHandler; +import com.android.wm.shell.compatui.impl.DefaultCompatUIRepository; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -245,12 +247,13 @@ public abstract class WMShellBaseModule { Lazy<DockStateReader> dockStateReader, Lazy<CompatUIConfiguration> compatUIConfiguration, Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler, - Lazy<AccessibilityManager> accessibilityManager) { + Lazy<AccessibilityManager> accessibilityManager, + CompatUIRepository compatUIRepository) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } if (Flags.appCompatUiFramework()) { - return Optional.of(new DefaultCompatUIHandler()); + return Optional.of(new DefaultCompatUIHandler(compatUIRepository)); } return Optional.of( new CompatUIController( @@ -271,6 +274,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static CompatUIRepository provideCompatUIRepository() { + return new DefaultCompatUIRepository(); + } + + @WMSingleton + @Provides static SyncTransactionQueue provideSyncTransactionQueue(TransactionPool pool, @ShellMainThread ShellExecutor mainExecutor) { return new SyncTransactionQueue(pool, mainExecutor); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index ca0586418041..247cc42e51ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -88,14 +88,17 @@ class DesktopModeTaskRepository { /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { visibleTasksListeners[visibleTasksListener] = executor - displayData.keyIterator().forEach { displayId -> - val visibleTasksCount = getVisibleTaskCount(displayId) + displayData.keyIterator().forEach { executor.execute { - visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) + visibleTasksListener.onTasksVisibilityChanged(it, visibleTaskCount(it)) } } } + /** Returns a list of all [DisplayData]. */ + private fun displayDataList(): Sequence<DisplayData> = + displayData.valueIterator().asSequence() + /** * Add a Consumer which will inform other classes of changes to exclusion regions for all * Desktop tasks. @@ -208,37 +211,17 @@ class DesktopModeTaskRepository { return removed } - /** Check if a task with the given [taskId] was marked as an active task */ - fun isActiveTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.activeTasks.contains(taskId) - } - } - - /** Check if a task with the given [taskId] was marked as a closing task */ - fun isClosingTask(taskId: Int): Boolean = - displayData.valueIterator().asSequence().any { data -> taskId in data.closingTasks } - - /** Whether a task is visible. */ - fun isVisibleTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.visibleTasks.contains(taskId) - } - } - - /** Return whether the given Task is minimized. */ - fun isMinimizedTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.minimizedTasks.contains(taskId) - } - } + fun isActiveTask(taskId: Int) = displayDataList().any { taskId in it.activeTasks } + fun isClosingTask(taskId: Int) = displayDataList().any { taskId in it.closingTasks } + fun isVisibleTask(taskId: Int) = displayDataList().any { taskId in it.visibleTasks } + fun isMinimizedTask(taskId: Int) = displayDataList().any { taskId in it.minimizedTasks } /** * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task * on its display */ fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean = - displayData.valueIterator().asSequence().any { data -> + displayDataList().any { data -> data.visibleTasks .subtract(data.closingTasks) .subtract(data.minimizedTasks) @@ -255,12 +238,6 @@ class DesktopModeTaskRepository { ArraySet(displayData[displayId]?.minimizedTasks) /** - * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks - * are visible. - */ - fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 - - /** * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, * ordered from front to back. */ @@ -305,14 +282,14 @@ class DesktopModeTaskRepository { return } - val prevCount = getVisibleTaskCount(displayId) + val prevCount = visibleTaskCount(displayId) if (visible) { displayData.getOrCreate(displayId).visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { displayData[displayId]?.visibleTasks?.remove(taskId) } - val newCount = getVisibleTaskCount(displayId) + val newCount = visibleTaskCount(displayId) // Check if count changed if (prevCount != newCount) { @@ -340,7 +317,7 @@ class DesktopModeTaskRepository { } /** Get number of tasks that are marked as visible on given [displayId] */ - fun getVisibleTaskCount(displayId: Int): Int { + fun visibleTaskCount(displayId: Int): Int { ProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: visibleTaskCount= %d", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 580724666949..9a1a8a20ae0e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -246,10 +246,12 @@ class DesktopTasksController( } } - /** Get number of tasks that are marked as visible */ - fun getVisibleTaskCount(displayId: Int): Int { - return desktopModeTaskRepository.getVisibleTaskCount(displayId) - } + /** Gets number of visible tasks in [displayId]. */ + fun visibleTaskCount(displayId: Int): Int = + desktopModeTaskRepository.visibleTaskCount(displayId) + + /** Returns true if any tasks are visible in Desktop Mode. */ + fun isDesktopModeShowing(displayId: Int): Boolean = visibleTaskCount(displayId) > 0 /** Enter desktop by using the focused task in given `displayId` */ fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { @@ -981,7 +983,7 @@ class DesktopTasksController( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked") return null } - if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { + if (!isDesktopModeShowing(task.displayId)) { ProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bring desktop tasks to front on transition" + @@ -1012,7 +1014,7 @@ class DesktopTasksController( transition: IBinder ): WindowContainerTransaction? { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") - if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { + if (isDesktopModeShowing(task.displayId)) { ProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch fullscreen task to freeform on transition" + @@ -1045,14 +1047,12 @@ class DesktopTasksController( /** Handle task closing by removing wallpaper activity if it's the last active task */ private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { - val wct = if ( - desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) && - desktopModeTaskRepository.wallpaperActivityToken != null - ) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleTaskClosing") + val wct = WindowContainerTransaction() + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) + && desktopModeTaskRepository.wallpaperActivityToken != null) { // Remove wallpaper activity when the last active task is removed - WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) } - } else { - null + removeWallpaperActivity(wct) } if (!desktopModeTaskRepository.addClosingTask(task.displayId, task.taskId)) { // Could happen if the task hasn't been removed from closing list after it disappeared @@ -1062,7 +1062,12 @@ class DesktopTasksController( task.taskId ) } - return wct + // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task. + if (Flags.enableDesktopWindowingBackNavigation() && + desktopModeTaskRepository.isVisibleTask(task.taskId)) { + wct.removeTask(task.token) + } + return if (wct.isEmpty) null else wct } private fun addMoveToDesktopChanges( @@ -1393,8 +1398,7 @@ class DesktopTasksController( onFinishCallback: Consumer<Boolean> ): Boolean { // TODO(b/320797628): Pass through which display we are dropping onto - val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY) - if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + if (!isDesktopModeShowing(DEFAULT_DISPLAY)) { // Not currently in desktop mode, ignore the drop return false } @@ -1553,8 +1557,8 @@ class DesktopTasksController( val result = IntArray(1) executeRemoteCallWithTaskPermission( controller, - "getVisibleTaskCount", - { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, + "visibleTaskCount", + { controller -> result[0] = controller.visibleTaskCount(displayId) }, true /* blocking */ ) return result[0] diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index ff40d4c6e231..8d63ff2a3a5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1980,12 +1980,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } clearContentOverlay(); } - if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { - // Avoid double removal, which is fatal. - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface); - return; - } if (surface == null || !surface.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: trying to remove invalid content overlay (%s)", TAG, surface); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 41042344fd3a..9bcd9b0a11c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -3573,7 +3573,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); pw.println(innerPrefix + "isSplitActive=" + isSplitActive()); pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible()); - pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit()); + pw.println(innerPrefix + "isLeftRightSplit=" + + (mSplitLayout != null ? mSplitLayout.isLeftRightSplit() : "null")); pw.println(innerPrefix + "MainStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + mMainStage.isActive()); @@ -3585,7 +3586,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); - mSplitLayout.dump(pw, childPrefix); + if (mSplitLayout != null) { + mSplitLayout.dump(pw, childPrefix); + } if (!mPausingTasks.isEmpty()) { pw.println(childPrefix + "mPausingTasks=" + mPausingTasks); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 4f4b8097cfac..766a6b3f48ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -353,7 +353,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, return this::setRecentsTransitionDuringKeyguard; } else if (mDesktopTasksController != null // Check on the default display. Recents/gesture nav is only available there - && mDesktopTasksController.getVisibleTaskCount(DEFAULT_DISPLAY) > 0) { + && mDesktopTasksController.visibleTaskCount(DEFAULT_DISPLAY) > 0) { return this::setRecentsTransitionDuringDesktop; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 153221150992..b5b476d90d0e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW; import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW; import android.graphics.Point; @@ -103,6 +104,9 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); mTaskOrganizer.applyTransaction(wct); } + } else { + mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface, + mDesktopWindowDecoration.mContext, CUJ_DESKTOP_MODE_DRAG_WINDOW); } mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId); mRepositionTaskBounds.set(mTaskBoundsAtDragStart); @@ -157,11 +161,16 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, } mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_RESIZE_WINDOW); } else { - final WindowContainerTransaction wct = new WindowContainerTransaction(); DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); - wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); - mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + if (!mTaskBoundsAtDragStart.equals(mRepositionTaskBounds)) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); + mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + } else { + // Drag-move ended where it originally started, no need to update WM. + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW); + } } mCtrlType = CTRL_TYPE_UNDEFINED; @@ -202,6 +211,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mCtrlType = CTRL_TYPE_UNDEFINED; finishCallback.onTransitionFinished(null); mIsResizingOrAnimatingResize = false; + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW); return true; } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt new file mode 100644 index 000000000000..bbf0ce5f8165 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt @@ -0,0 +1,66 @@ +/* + * 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.wm.shell.flicker.service.desktopmode.scenarios + +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +/** Base scenario test for window drag CUJ with multiple windows. */ +@Ignore("Base Test Class") +abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase() +{ + private val imeAppHelper = ImeAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + override fun dragAppWindow() { + val (startXIme, startYIme) = getWindowDragStartCoordinate(imeAppHelper) + + imeApp.dragWindow(startXIme, startYIme, + endX = startXIme + 150, endY = startYIme + 150, + wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt new file mode 100644 index 000000000000..a613ca1660ea --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt @@ -0,0 +1,55 @@ +/* + * 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.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** Base test class for window drag CUJ. */ +@Ignore("Base Test Class") +abstract class DragAppWindowScenarioTestBase { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Test abstract fun dragAppWindow() + + /** Return the top-center coordinate of the app header as the start coordinate. */ + fun getWindowDragStartCoordinate(appHelper: StandardAppHelper): Pair<Int, Int> { + val windowRect = wmHelper.getWindowRegion(appHelper).bounds + // Set start x-coordinate as center of app header. + val startX = windowRect.centerX() + val startY = windowRect.top + return Pair(startX, startY) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt new file mode 100644 index 000000000000..0655620d58b7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt @@ -0,0 +1,53 @@ +/* + * 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.wm.shell.flicker.service.desktopmode.scenarios + +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +/** Base scenario test for window drag CUJ with single window. */ +@Ignore("Base Test Class") +abstract class DragAppWindowSingleWindow : DragAppWindowScenarioTestBase() +{ + private val simpleAppHelper = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(simpleAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + override fun dragAppWindow() { + val (startXTest, startYTest) = getWindowDragStartCoordinate(simpleAppHelper) + testApp.dragWindow(startXTest, startYTest, + endX = startXTest + 150, endY = startYTest + 150, + wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt index 8bf011192347..080ad901c656 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.RemoteException import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.Choreographer import android.view.RemoteAnimationTarget import android.view.SurfaceControl import android.view.SurfaceControl.Transaction @@ -37,8 +36,6 @@ import androidx.test.filters.SmallTest import com.android.internal.policy.TransitionAnimation import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit import junit.framework.TestCase.assertEquals import org.junit.Assert import org.junit.Before @@ -50,12 +47,13 @@ import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit @SmallTest @TestableLooper.RunWithLooper @@ -82,7 +80,6 @@ class CustomCrossActivityBackAnimationTest : ShellTestCase() { backAnimationBackground, rootTaskDisplayAreaOrganizer, transaction, - mock(Choreographer::class.java), customAnimationLoader ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt new file mode 100644 index 000000000000..1a86cfd6b69e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt @@ -0,0 +1,89 @@ +/* + * 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.wm.shell.compatui.impl + +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for {@link DefaultCompatUIRepository}. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultCompatUIRepositoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DefaultCompatUIRepositoryTest { + + lateinit var repository: CompatUIRepository + + @get:Rule + val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Before + fun setUp() { + repository = DefaultCompatUIRepository() + } + + @Test(expected = IllegalStateException::class) + fun `addSpec throws exception with specs with duplicate id`() { + repository.addSpec(CompatUISpec("one")) + repository.addSpec(CompatUISpec("one")) + } + + @Test + fun `iterateOn invokes the consumer`() { + with(repository) { + addSpec(CompatUISpec("one")) + addSpec(CompatUISpec("two")) + addSpec(CompatUISpec("three")) + val consumer = object : (CompatUISpec) -> Unit { + var acc = "" + override fun invoke(spec: CompatUISpec) { + acc += spec.name + } + } + iterateOn(consumer) + assertEquals("onetwothree", consumer.acc) + } + } + + @Test + fun `findSpec returns existing specs`() { + with(repository) { + val one = CompatUISpec("one") + val two = CompatUISpec("two") + val three = CompatUISpec("three") + addSpec(one) + addSpec(two) + addSpec(three) + assertEquals(findSpec("one"), one) + assertEquals(findSpec("two"), two) + assertEquals(findSpec("three"), three) + assertNull(findSpec("abc")) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt new file mode 100644 index 000000000000..cdc524aff09f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.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.wm.shell.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Fake implementation for {@link CompatUIRepository} + */ +class FakeCompatUIRepository : CompatUIRepository { + val allSpecs = mutableMapOf<String, CompatUISpec>() + override fun addSpec(spec: CompatUISpec) { + if (findSpec(spec.name) != null) { + throw IllegalStateException("Spec with name:${spec.name} already present") + } + allSpecs[spec.name] = spec + } + + override fun iterateOn(fn: (CompatUISpec) -> Unit) = + allSpecs.values.forEach(fn) + + override fun findSpec(name: String): CompatUISpec? = + allSpecs[name] +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 6612aee0cd12..18b08bfb0f47 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -337,65 +337,65 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun getVisibleTaskCount() { + fun visibleTaskCount_defaultDisplay_returnsCorrectCount() { // No tasks, count is 0 - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) // New task increments count to 1 repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Visibility update to same task does not increase count repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Second task visible increments count repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) // Hiding a task decrements count repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Hiding all tasks leaves count at 0 repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false) - assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0) + assertThat(repo.visibleTaskCount(displayId = 9)).isEqualTo(0) // Hiding a not existing task, count remains at 0 repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 999, visible = false) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @Test - fun getVisibleTaskCount_multipleDisplays() { - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) + fun visibleTaskCount_multipleDisplays_returnsCorrectCount() { + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on default display increments count for that display only repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on secondary display, increments count for that display only repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 2, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) // Marking task visible on another display, updates counts for both displays repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Marking task that is on secondary display, hidden on default display, does not affect // secondary display repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Hiding a task on that display, decrements count repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = false) - assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) } @Test @@ -494,28 +494,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun isDesktopModeShowing_noActiveTasks_returnsFalse() { - assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_noTasksVisible_returnsFalse() { - repo.addActiveTask(displayId = 0, taskId = 1) - repo.addActiveTask(displayId = 0, taskId = 2) - - assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { - repo.addActiveTask(displayId = 0, taskId = 1) - repo.addActiveTask(displayId = 0, taskId = 2) - repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true) - - assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue() - } - - @Test fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() { repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 8558a77e4e7e..da886863cc1f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -20,7 +20,6 @@ import android.app.ActivityManager.RecentTaskInfo import android.app.ActivityManager.RunningTaskInfo import android.app.KeyguardManager import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME -import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN @@ -68,6 +67,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.MockToken import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -125,11 +125,11 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever /** * Test class for {@link DesktopTasksController} @@ -311,6 +311,31 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun isDesktopModeShowing_noTasks_returnsFalse() { + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) @@ -526,32 +551,32 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun getVisibleTaskCount_noTasks_returnsZero() { - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + fun visibleTaskCount_noTasks_returnsZero() { + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @Test - fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() { setUpHomeTask() setUpFreeformTask().also(::markTaskVisible) setUpFreeformTask().also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) } @Test - fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + fun visibleTaskCount_twoTasks_oneVisible_returnsOne() { setUpHomeTask() setUpFreeformTask().also(::markTaskVisible) setUpFreeformTask().also(::markTaskHidden) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) } @Test - fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { setUpHomeTask() setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) } @Test @@ -1451,7 +1476,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .build() - val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) + val transition = createTransition(task = task, type = TRANSIT_CLOSE) val result = controller.handleRequest(Binder(), transition) assertThat(result).isNull() } @@ -1545,8 +1570,11 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + ) + fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -1555,8 +1583,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -1565,8 +1607,11 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task = setUpFreeformTask() desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() @@ -1576,22 +1621,42 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1602,8 +1667,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasks_backNavigationDisabled_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1614,8 +1695,28 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1624,14 +1725,33 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1640,14 +1760,36 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() { + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_nonMinimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + desktopModeTaskRepository + .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false) + val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -1656,8 +1798,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -1666,8 +1822,11 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task = setUpFreeformTask() desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() @@ -1677,22 +1836,42 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1703,8 +1882,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request") + result.assertRemoveAt(index = 0, task1.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksFlagEnabled_backNavigationDisabled_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1715,8 +1911,28 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1725,14 +1941,33 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasksOneNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1741,9 +1976,28 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - assertNotNull(result, "Should handle request") - // Should create remove wallpaper transaction - .assertRemoveAt(index = 0, wallpaperToken) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_minimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + desktopModeTaskRepository + .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false) + val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") } @Test diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index d42b25681e11..d20b7f090e92 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -762,14 +762,14 @@ interface IAudioService { void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher); - oneway void startLoudnessCodecUpdates(int sessionId); + void startLoudnessCodecUpdates(int sessionId); - oneway void stopLoudnessCodecUpdates(int sessionId); + void stopLoudnessCodecUpdates(int sessionId); - oneway void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, + void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, in LoudnessCodecInfo codecInfo); - oneway void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo); + void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo); PersistableBundle getLoudnessParams(in LoudnessCodecInfo codecInfo); diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 40592915a3a8..999f40e53952 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -312,6 +312,10 @@ public final class MediaProjection { * <p>Once a MediaProjection has been stopped, it's up to the application to release any * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and * {@link Surface}). + * + * <p>After this callback any call to + * {@link MediaProjection#createVirtualDisplay} will fail, even if no such + * {@link VirtualDisplay} was ever created for this MediaProjection session. */ public void onStop() { } diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 7ed67dcde913..4013d844d811 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -43,25 +43,31 @@ import java.util.Map; /** * Manages the retrieval of certain types of {@link MediaProjection} tokens. * - * <p><ol>An example flow of starting a media projection will be: - * <li>Declare a foreground service with the type {@code mediaProjection} in - * the {@code AndroidManifest.xml}. - * </li> - * <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()} - * and pass this intent to {@link Activity#startActivityForResult(Intent, int)}. - * </li> - * <li>On getting {@link Activity#onActivityResult(int, int, Intent)}, - * start the foreground service with the type - * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}. - * </li> - * <li>Retrieve the media projection token by calling - * {@link MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and - * intent from the {@link Activity#onActivityResult(int, int, Intent)} above. - * </li> - * <li>Start the screen capture session for media projection by calling - * {@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface, - * android.hardware.display.VirtualDisplay.Callback, Handler)}. - * </li> + * <p> + * + * <ol> + * An example flow of starting a media projection will be: + * <li>Declare a foreground service with the type {@code mediaProjection} in the {@code + * AndroidManifest.xml}. + * <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()} and + * pass this intent to {@link Activity#startActivityForResult(Intent, int)}. + * <li>On getting {@link Activity#onActivityResult(int, int, Intent)}, start the foreground + * service with the type {@link + * android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}. + * <li>Retrieve the media projection token by calling {@link + * MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and intent + * from the {@link Activity#onActivityResult(int, int, Intent)} above. + * <li>Register a {@link MediaProjection.Callback} by calling {@link + * MediaProjection#registerCallback(MediaProjection.Callback, Handler)}. This is required to + * receive notifications about when the {@link MediaProjection} or captured content changes + * state. When receiving an `onStop()` callback, the client must clean up any resources it is + * holding, e.g. the {@link VirtualDisplay} and {@link Surface}. The MediaProjection may + * further no longer create any new {@link VirtualDisplay}s via {@link + * MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface, + * VirtualDisplay.Callback, Handler)}. + * <li>Start the screen capture session for media projection by calling {@link + * MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface, + * android.hardware.display.VirtualDisplay.Callback, Handler)}. * </ol> */ @SystemService(Context.MEDIA_PROJECTION_SERVICE) diff --git a/nfc/java/android/nfc/AvailableNfcAntenna.java b/nfc/java/android/nfc/AvailableNfcAntenna.java index 6e6512a04971..e76aeb07f106 100644 --- a/nfc/java/android/nfc/AvailableNfcAntenna.java +++ b/nfc/java/android/nfc/AvailableNfcAntenna.java @@ -28,13 +28,13 @@ import android.os.Parcelable; public final class AvailableNfcAntenna implements Parcelable { /** * Location of the antenna on the Y axis in millimeters. - * 0 is the bottom-left when the user is facing the screen + * 0 is the top-left when the user is facing the screen * and the device orientation is Portrait. */ private final int mLocationX; /** * Location of the antenna on the Y axis in millimeters. - * 0 is the bottom-left when the user is facing the screen + * 0 is the top-left when the user is facing the screen * and the device orientation is Portrait. */ private final int mLocationY; @@ -46,7 +46,7 @@ public final class AvailableNfcAntenna implements Parcelable { /** * Location of the antenna on the X axis in millimeters. - * 0 is the bottom-left when the user is facing the screen + * 0 is the top-left when the user is facing the screen * and the device orientation is Portrait. */ public int getLocationX() { @@ -55,7 +55,7 @@ public final class AvailableNfcAntenna implements Parcelable { /** * Location of the antenna on the Y axis in millimeters. - * 0 is the bottom-left when the user is facing the screen + * 0 is the top-left when the user is facing the screen * and the device orientation is Portrait. */ public int getLocationY() { diff --git a/nfc/java/android/nfc/NfcAntennaInfo.java b/nfc/java/android/nfc/NfcAntennaInfo.java index b002ca21e8e3..c57b2e029cc5 100644 --- a/nfc/java/android/nfc/NfcAntennaInfo.java +++ b/nfc/java/android/nfc/NfcAntennaInfo.java @@ -64,9 +64,9 @@ public final class NfcAntennaInfo implements Parcelable { /** * Whether the device is foldable. When the device is foldable, - * the 0, 0 is considered to be bottom-left when the device is unfolded and + * the 0, 0 is considered to be top-left when the device is unfolded and * the screens are facing the user. For non-foldable devices 0, 0 - * is bottom-left when the user is facing the screen. + * is top-left when the user is facing the screen. */ public boolean isDeviceFoldable() { return mDeviceFoldable; diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml index df5644b8aad0..26453609fc43 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -46,20 +46,6 @@ </intent-filter> </provider> - <provider android:name="com.android.settingslib.spa.slice.SpaSliceProvider" - android:authorities="com.android.spa.gallery.slice.provider" - android:exported="true" > - <intent-filter> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.app.slice.category.SLICE" /> - </intent-filter> - </provider> - - <receiver - android:name="com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver" - android:exported="false"> - </receiver> - <activity android:name="com.android.settingslib.spa.debug.BlankActivity" android:exported="true"> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 91bd7916b0ab..ffd28798d82f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -55,7 +55,6 @@ import com.android.settingslib.spa.gallery.ui.CategoryPageProvider import com.android.settingslib.spa.gallery.ui.CopyablePageProvider import com.android.settingslib.spa.gallery.scaffold.ScrollablePagerPageProvider import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider -import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver /** * Enum to define all SPP name here. @@ -120,9 +119,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { override val logger = DebugLogger() override val browseActivityClass = GalleryMainActivity::class.java - override val sliceBroadcastReceiverClass = SpaSliceBroadcastReceiver::class.java // For debugging override val searchProviderAuthorities = "com.android.spa.gallery.search.provider" - override val sliceProviderAuthorities = "com.android.spa.gallery.slice.provider" } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt index 96de1a778c97..6d1d34628efa 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.EntrySliceData import com.android.settingslib.spa.framework.common.EntryStatusData import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder @@ -35,10 +34,8 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.framework.util.createIntent import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_SUMMARY import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY @@ -48,15 +45,10 @@ import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Compan import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE -import com.android.settingslib.spa.slice.createBrowsePendingIntent -import com.android.settingslib.spa.slice.provider.createDemoActionSlice -import com.android.settingslib.spa.slice.provider.createDemoBrowseSlice -import com.android.settingslib.spa.slice.provider.createDemoSlice import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro import com.android.settingslib.spa.widget.ui.SettingsIcon -import kotlinx.coroutines.delay private const val TAG = "PreferencePage" @@ -139,26 +131,6 @@ object PreferencePageProvider : SettingsPageProvider { override val enabled = { model.asyncEnable.value } } ) - } - .setSliceDataFn { sliceUri, _ -> - val createSliceImpl = { s: String -> - createDemoBrowseSlice( - sliceUri = sliceUri, - title = ASYNC_PREFERENCE_TITLE, - summary = s, - ) - } - return@setSliceDataFn object : EntrySliceData() { - init { - postValue(createSliceImpl("(loading)")) - } - - override suspend fun asyncRunner() { - spaLogger.message(TAG, "Async entry loading") - delay(2000L) - postValue(createSliceImpl(ASYNC_PREFERENCE_SUMMARY)) - } - } }.build() ) entryList.add( @@ -176,28 +148,6 @@ object PreferencePageProvider : SettingsPageProvider { } } ) - } - .setSliceDataFn { sliceUri, args -> - val createSliceImpl = { v: Int -> - createDemoActionSlice( - sliceUri = sliceUri, - title = MANUAL_UPDATE_PREFERENCE_TITLE, - summary = "manual update value $v", - ) - } - - return@setSliceDataFn object : EntrySliceData() { - private var tick = args?.getString("init")?.toInt() ?: 0 - - init { - postValue(createSliceImpl(tick)) - } - - override suspend fun asyncAction() { - tick++ - postValue(createSliceImpl(tick)) - } - } }.build() ) entryList.add( @@ -216,33 +166,6 @@ object PreferencePageProvider : SettingsPageProvider { } } ) - } - .setSliceDataFn { sliceUri, args -> - val createSliceImpl = { v: Int -> - createDemoBrowseSlice( - sliceUri = sliceUri, - title = AUTO_UPDATE_PREFERENCE_TITLE, - summary = "auto update value $v", - ) - } - - return@setSliceDataFn object : EntrySliceData() { - private var tick = args?.getString("init")?.toInt() ?: 0 - - init { - postValue(createSliceImpl(tick)) - } - - override suspend fun asyncRunner() { - spaLogger.message(TAG, "autoUpdater.active") - while (true) { - delay(1000L) - tick++ - spaLogger.message(TAG, "autoUpdater.value $tick") - postValue(createSliceImpl(tick)) - } - } - } }.build() ) @@ -272,22 +195,6 @@ object PreferencePageProvider : SettingsPageProvider { clickRoute = SettingsPageProviderEnum.PREFERENCE.name ) } - .setSliceDataFn { sliceUri, _ -> - val intent = owner.createIntent()?.createBrowsePendingIntent() - ?: return@setSliceDataFn null - return@setSliceDataFn object : EntrySliceData() { - init { - postValue( - createDemoSlice( - sliceUri = sliceUri, - title = PAGE_TITLE, - summary = "Injected Entry", - intent = intent, - ) - ) - } - } - } } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt index 2d956d5eddb2..6e5132bbb53e 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt @@ -17,12 +17,10 @@ package com.android.settingslib.spa.framework.common import android.app.Activity -import android.content.BroadcastReceiver import android.content.Context import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import com.android.settingslib.spa.slice.SettingsSliceDataRepository private const val TAG = "SpaEnvironment" @@ -69,8 +67,6 @@ abstract class SpaEnvironment(context: Context) { val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) } - val sliceDataRepository = lazy { SettingsSliceDataRepository(entryRepository.value) } - // The application context. Use local context as fallback when applicationContext is not // available (e.g. in Robolectric test). val appContext: Context = context.applicationContext ?: context @@ -81,11 +77,9 @@ abstract class SpaEnvironment(context: Context) { // Specify class name of browse activity and slice broadcast receiver, which is used to // generate the necessary intents. open val browseActivityClass: Class<out Activity>? = null - open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null // Specify provider authorities for debugging purpose. open val searchProviderAuthorities: String? = null - open val sliceProviderAuthorities: String? = null // TODO: add other environment setup here. companion object { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt deleted file mode 100644 index 7a4750dfb134..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice - -import android.net.Uri -import android.util.Log -import com.android.settingslib.spa.framework.common.EntrySliceData -import com.android.settingslib.spa.framework.common.SettingsEntryRepository -import com.android.settingslib.spa.framework.util.getEntryId - -private const val TAG = "SliceDataRepository" - -class SettingsSliceDataRepository(private val entryRepository: SettingsEntryRepository) { - // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?> - private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf() - - // Note: mark this function synchronized, so that we can get the same livedata during the - // whole lifecycle of a Slice. - @Synchronized - fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? { - val sliceString = sliceUri.getSliceId() ?: return null - return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let { - sliceDataMap[sliceString] = it - it - } - } - - fun getActiveSliceData(sliceUri: Uri): EntrySliceData? { - val sliceString = sliceUri.getSliceId() ?: return null - val sliceData = sliceDataMap[sliceString] ?: return null - return if (sliceData.isActive()) sliceData else null - } - - private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? { - Log.d(TAG, "buildLiveData: $sliceUri") - - val entryId = sliceUri.getEntryId() ?: return null - val entry = entryRepository.getEntry(entryId) ?: return null - if (!entry.hasSliceSupport) return null - val arguments = sliceUri.getRuntimeArguments() - return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri) - } -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt index f3628903dc6d..ec89c7cd3a6c 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt @@ -16,23 +16,10 @@ package com.android.settingslib.spa.slice -import android.app.Activity -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context -import android.content.Intent import android.net.Uri import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.util.KEY_DESTINATION import com.android.settingslib.spa.framework.util.KEY_HIGHLIGHT_ENTRY -import com.android.settingslib.spa.framework.util.SESSION_SLICE -import com.android.settingslib.spa.framework.util.SPA_INTENT_RESERVED_KEYS -import com.android.settingslib.spa.framework.util.appendSpaParams -import com.android.settingslib.spa.framework.util.getDestination -import com.android.settingslib.spa.framework.util.getEntryId // Defines SliceUri, which contains special query parameters: // -- KEY_DESTINATION: The route that this slice is navigated to. @@ -45,25 +32,6 @@ fun SliceUri.getEntryId(): String? { return getQueryParameter(KEY_HIGHLIGHT_ENTRY) } -fun SliceUri.getDestination(): String? { - return getQueryParameter(KEY_DESTINATION) -} - -fun SliceUri.getRuntimeArguments(): Bundle { - val params = Bundle() - for (queryName in queryParameterNames) { - if (SPA_INTENT_RESERVED_KEYS.contains(queryName)) continue - params.putString(queryName, getQueryParameter(queryName)) - } - return params -} - -fun SliceUri.getSliceId(): String? { - val entryId = getEntryId() ?: return null - val params = getRuntimeArguments() - return "${entryId}_$params" -} - fun Uri.Builder.appendSpaParams( destination: String? = null, entryId: String? = null, @@ -79,72 +47,3 @@ fun Uri.Builder.appendSpaParams( return this } -fun Uri.Builder.fromEntry( - entry: SettingsEntry, - authority: String?, - runtimeArguments: Bundle? = null -): Uri.Builder { - if (authority == null) return this - val sp = entry.containerPage() - return scheme("content").authority(authority).appendSpaParams( - destination = sp.buildRoute(), - entryId = entry.id, - runtimeArguments = runtimeArguments - ) -} - -fun SliceUri.createBroadcastPendingIntent(): PendingIntent? { - val context = SpaEnvironmentFactory.instance.appContext - val sliceBroadcastClass = - SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null - val entryId = getEntryId() ?: return null - return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId) -} - -fun SliceUri.createBrowsePendingIntent(): PendingIntent? { - val context = SpaEnvironmentFactory.instance.appContext - val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null - val destination = getDestination() ?: return null - val entryId = getEntryId() - return createBrowsePendingIntent(context, browseActivityClass, destination, entryId) -} - -fun Intent.createBrowsePendingIntent(): PendingIntent? { - val context = SpaEnvironmentFactory.instance.appContext - val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null - val destination = getDestination() ?: return null - val entryId = getEntryId() - return createBrowsePendingIntent(context, browseActivityClass, destination, entryId) -} - -private fun createBrowsePendingIntent( - context: Context, - browseActivityClass: Class<out Activity>, - destination: String, - entryId: String? -): PendingIntent { - val intent = Intent().setComponent(ComponentName(context, browseActivityClass)) - .appendSpaParams(destination, entryId, SESSION_SLICE) - .apply { - // Set both extra and data (which is a Uri) in Slice Intent: - // 1) extra is used in SPA navigation framework - // 2) data is used in Slice framework - data = Uri.Builder().appendSpaParams(destination, entryId).build() - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) -} - -private fun createBroadcastPendingIntent( - context: Context, - sliceBroadcastClass: Class<out BroadcastReceiver>, - entryId: String -): PendingIntent { - val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass)) - .apply { data = Uri.Builder().appendSpaParams(entryId = entryId).build() } - return PendingIntent.getBroadcast( - context, 0 /* requestCode */, intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt deleted file mode 100644 index 39cb43180f58..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory - -class SpaSliceBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository - val sliceUri = intent?.data ?: return - val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return - sliceData.doAction() - } -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt deleted file mode 100644 index 3496f02a70e4..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice - -import android.net.Uri -import android.util.Log -import androidx.lifecycle.Observer -import androidx.slice.Slice -import androidx.slice.SliceProvider -import com.android.settingslib.spa.framework.common.EntrySliceData -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext - -private const val TAG = "SpaSliceProvider" - -class SpaSliceProvider : SliceProvider(), Observer<Slice?> { - private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? { - if (!SpaEnvironmentFactory.isReady()) return null - val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository - return sliceRepository.getOrBuildSliceData(sliceUri) - } - - override fun onBindSlice(sliceUri: Uri): Slice? { - if (context == null) return null - Log.d(TAG, "onBindSlice: $sliceUri") - return getOrPutSliceData(sliceUri)?.value - } - - override fun onSlicePinned(sliceUri: Uri) { - Log.d(TAG, "onSlicePinned: $sliceUri") - super.onSlicePinned(sliceUri) - val sliceLiveData = getOrPutSliceData(sliceUri) ?: return - runBlocking { - withContext(Dispatchers.Main) { - sliceLiveData.observeForever(this@SpaSliceProvider) - } - } - } - - override fun onSliceUnpinned(sliceUri: Uri) { - Log.d(TAG, "onSliceUnpinned: $sliceUri") - super.onSliceUnpinned(sliceUri) - val sliceLiveData = getOrPutSliceData(sliceUri) ?: return - runBlocking { - withContext(Dispatchers.Main) { - sliceLiveData.removeObserver(this@SpaSliceProvider) - } - } - } - - override fun onChanged(value: Slice?) { - val uri = value?.uri ?: return - Log.d(TAG, "onChanged: $uri") - context?.contentResolver?.notifyChange(uri, null) - } - - override fun onCreateSliceProvider(): Boolean { - Log.d(TAG, "onCreateSliceProvider") - return true - } -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt deleted file mode 100644 index 007f47bd3c82..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice.presenter - -import android.net.Uri -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.slice.widget.SliceLiveData -import androidx.slice.widget.SliceView - -@Composable -fun SliceDemo(sliceUri: Uri) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val sliceData = remember { - SliceLiveData.fromUri(context, sliceUri) - } - - HorizontalDivider() - AndroidView( - factory = { localContext -> - val view = SliceView(localContext) - view.setShowTitleItems(true) - view.isScrollable = false - view - }, - update = { view -> sliceData.observe(lifecycleOwner, view) } - ) -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt deleted file mode 100644 index e4a738631474..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice.provider - -import android.app.PendingIntent -import android.content.Context -import android.net.Uri -import androidx.core.R -import androidx.core.graphics.drawable.IconCompat -import androidx.slice.Slice -import androidx.slice.SliceManager -import androidx.slice.builders.ListBuilder -import androidx.slice.builders.SliceAction -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.slice.createBroadcastPendingIntent -import com.android.settingslib.spa.slice.createBrowsePendingIntent - -fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? { - val intent = sliceUri.createBrowsePendingIntent() ?: return null - return createDemoSlice(sliceUri, title, summary, intent) -} - -fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? { - val intent = sliceUri.createBroadcastPendingIntent() ?: return null - return createDemoSlice(sliceUri, title, summary, intent) -} - -fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? { - val context = SpaEnvironmentFactory.instance.appContext - if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null - return ListBuilder(context, sliceUri, ListBuilder.INFINITY) - .addRow(ListBuilder.RowBuilder().apply { - setPrimaryAction(createSliceAction(context, intent)) - setTitle(title) - setSubtitle(summary) - }).build() -} - -private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction { - return SliceAction.create( - intent, - IconCompat.createWithResource(context, R.drawable.notification_action_background), - ListBuilder.ICON_IMAGE, - "Enter app" - ) -} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt deleted file mode 100644 index 341a4a5134f9..000000000000 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.slice - -import android.content.Context -import android.net.Uri -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer -import androidx.slice.Slice -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.util.genEntryId -import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest -import com.android.settingslib.spa.tests.testutils.SppHome -import com.android.settingslib.spa.tests.testutils.SppLayer2 -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SettingsSliceDataRepositoryTest { - @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() - - private val context: Context = ApplicationProvider.getApplicationContext() - private val spaEnvironment = - SpaEnvironmentForTest(context, listOf(SppHome.createSettingsPage())) - private val sliceDataRepository by spaEnvironment.sliceDataRepository - - @Test - fun getOrBuildSliceDataTest() { - SpaEnvironmentFactory.reset(spaEnvironment) - - // Slice empty - assertThat(sliceDataRepository.getOrBuildSliceData(Uri.EMPTY)).isNull() - - // Slice supported - val page = SppLayer2.createSettingsPage() - val entryId = genEntryId("Layer2Entry1", page) - val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build() - assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2") - assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]") - val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri) - assertThat(sliceData).isNotNull() - assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData) - - // Slice unsupported - val entryId2 = genEntryId("Layer2Entry2", page) - val sliceUri2 = Uri.Builder().appendSpaParams(page.buildRoute(), entryId2).build() - assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2") - assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]") - assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri2)).isNull() - } - - @Test - fun getActiveSliceDataTest() { - SpaEnvironmentFactory.reset(spaEnvironment) - - val page = SppLayer2.createSettingsPage() - val entryId = genEntryId("Layer2Entry1", page) - val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build() - - // build slice data first - val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri) - - // slice data is inactive - assertThat(sliceData!!.isActive()).isFalse() - assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull() - - // slice data is active - val observer = Observer<Slice?> { } - sliceData.observeForever(observer) - assertThat(sliceData.isActive()).isTrue() - assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isSameInstanceAs(sliceData) - - // slice data is inactive again - sliceData.removeObserver(observer) - assertThat(sliceData.isActive()).isFalse() - assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull() - } -} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt index d1c4e5110f60..b489afdae157 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt @@ -16,91 +16,27 @@ package com.android.settingslib.spa.slice -import android.content.Context -import android.content.Intent import android.net.Uri import androidx.core.os.bundleOf -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SliceUtilTest { - private val context: Context = ApplicationProvider.getApplicationContext() - private val spaEnvironment = SpaEnvironmentForTest(context) - @Test fun sliceUriTest() { assertThat(Uri.EMPTY.getEntryId()).isNull() - assertThat(Uri.EMPTY.getDestination()).isNull() - assertThat(Uri.EMPTY.getRuntimeArguments().size()).isEqualTo(0) - assertThat(Uri.EMPTY.getSliceId()).isNull() // valid slice uri val dest = "myRoute" val entryId = "myEntry" val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build() assertThat(sliceUriWithoutParams.getEntryId()).isEqualTo(entryId) - assertThat(sliceUriWithoutParams.getDestination()).isEqualTo(dest) - assertThat(sliceUriWithoutParams.getRuntimeArguments().size()).isEqualTo(0) - assertThat(sliceUriWithoutParams.getSliceId()).isEqualTo("${entryId}_Bundle[{}]") val sliceUriWithParams = Uri.Builder().appendSpaParams(dest, entryId, bundleOf("p1" to "v1")).build() assertThat(sliceUriWithParams.getEntryId()).isEqualTo(entryId) - assertThat(sliceUriWithParams.getDestination()).isEqualTo(dest) - assertThat(sliceUriWithParams.getRuntimeArguments().size()).isEqualTo(1) - assertThat(sliceUriWithParams.getSliceId()).isEqualTo("${entryId}_Bundle[{p1=v1}]") - } - - @Test - fun createBroadcastPendingIntentTest() { - SpaEnvironmentFactory.reset(spaEnvironment) - - // Empty Slice Uri - assertThat(Uri.EMPTY.createBroadcastPendingIntent()).isNull() - - // Valid Slice Uri - val dest = "myRoute" - val entryId = "myEntry" - val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build() - val pendingIntent = sliceUriWithoutParams.createBroadcastPendingIntent() - assertThat(pendingIntent).isNotNull() - assertThat(pendingIntent!!.isBroadcast).isTrue() - assertThat(pendingIntent.isImmutable).isFalse() - } - - @Test - fun createBrowsePendingIntentTest() { - SpaEnvironmentFactory.reset(spaEnvironment) - - // Empty Slice Uri - assertThat(Uri.EMPTY.createBrowsePendingIntent()).isNull() - - // Empty Intent - assertThat(Intent().createBrowsePendingIntent()).isNull() - - // Valid Slice Uri - val dest = "myRoute" - val entryId = "myEntry" - val sliceUri = Uri.Builder().appendSpaParams(dest, entryId).build() - val pendingIntent = sliceUri.createBrowsePendingIntent() - assertThat(pendingIntent).isNotNull() - assertThat(pendingIntent!!.isActivity).isTrue() - assertThat(pendingIntent.isImmutable).isTrue() - - // Valid Intent - val intent = Intent().apply { - putExtra("spaActivityDestination", dest) - putExtra("highlightEntry", entryId) - } - val pendingIntent2 = intent.createBrowsePendingIntent() - assertThat(pendingIntent2).isNotNull() - assertThat(pendingIntent2!!.isActivity).isTrue() - assertThat(pendingIntent2.isImmutable).isTrue() } }
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt index 22a5ca328755..4f8fd794b248 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt @@ -216,8 +216,6 @@ class SpaEnvironmentForTest( context: Context, rootPages: List<SettingsPage> = emptyList(), override val browseActivityClass: Class<out Activity>? = BlankActivity::class.java, - override val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = - BlankSliceBroadcastReceiver::class.java, override val logger: SpaLogger = object : SpaLogger {} ) : SpaEnvironment(context) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt new file mode 100644 index 000000000000..2eaa804152e4 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.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.settingslib.bluetooth + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +/** [Flow] for [BluetoothCallback] device profile connection state change events */ +val BluetoothEventManager.onProfileConnectionStateChanged: Flow<ProfileConnectionState> + get() = callbackFlow { + val callback = + object : BluetoothCallback { + override fun onProfileConnectionStateChanged( + cachedDevice: CachedBluetoothDevice, + @BluetoothCallback.ConnectionState state: Int, + bluetoothProfile: Int + ) { + launch { send(ProfileConnectionState(cachedDevice, state, bluetoothProfile)) } + } + } + registerCallback(callback) + awaitClose { unregisterCallback(callback) } + } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt new file mode 100644 index 000000000000..91a99aed6db5 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt @@ -0,0 +1,85 @@ +/* + * 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.settingslib.bluetooth + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothLeBroadcastAssistant +import android.bluetooth.BluetoothLeBroadcastMetadata +import android.bluetooth.BluetoothLeBroadcastReceiveState +import com.android.internal.util.ConcurrentUtils +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +/** [Flow] for [BluetoothLeBroadcastAssistant.Callback] source connected/removed events */ +val LocalBluetoothLeBroadcastAssistant.onSourceConnectedOrRemoved: Flow<Unit> + get() = callbackFlow { + val callback = + object : BluetoothLeBroadcastAssistant.Callback { + override fun onReceiveStateChanged( + sink: BluetoothDevice, + sourceId: Int, + state: BluetoothLeBroadcastReceiveState + ) { + if (BluetoothUtils.isConnected(state)) { + launch { send(Unit) } + } + } + + override fun onSourceRemoved(sink: BluetoothDevice, sourceId: Int, reason: Int) { + launch { send(Unit) } + } + + override fun onSearchStarted(reason: Int) {} + + override fun onSearchStartFailed(reason: Int) {} + + override fun onSearchStopped(reason: Int) {} + + override fun onSearchStopFailed(reason: Int) {} + + override fun onSourceFound(source: BluetoothLeBroadcastMetadata) {} + + override fun onSourceAdded(sink: BluetoothDevice, sourceId: Int, reason: Int) {} + + override fun onSourceAddFailed( + sink: BluetoothDevice, + source: BluetoothLeBroadcastMetadata, + reason: Int + ) {} + + override fun onSourceModified(sink: BluetoothDevice, sourceId: Int, reason: Int) {} + + override fun onSourceModifyFailed( + sink: BluetoothDevice, + sourceId: Int, + reason: Int + ) {} + + override fun onSourceRemoveFailed( + sink: BluetoothDevice, + sourceId: Int, + reason: Int + ) {} + } + registerServiceCallBack( + ConcurrentUtils.DIRECT_EXECUTOR, + callback, + ) + awaitClose { unregisterServiceCallBack(callback) } + } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt new file mode 100644 index 000000000000..45aaa66e7f95 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt @@ -0,0 +1,23 @@ +/* + * 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.settingslib.bluetooth + +data class ProfileConnectionState( + val cachedDevice: CachedBluetoothDevice, + @BluetoothCallback.ConnectionState val state: Int, + val bluetoothProfile: Int, +)
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index ef9452648a70..b2fcb5f6da41 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -16,7 +16,9 @@ package com.android.settingslib.notification.data.repository +import android.annotation.SuppressLint import android.app.NotificationManager +import android.app.NotificationManager.EXTRA_NOTIFICATION_POLICY import android.content.BroadcastReceiver import android.content.ContentResolver import android.content.Context @@ -57,6 +59,7 @@ interface ZenModeRepository { val modes: Flow<List<ZenMode>> } +@SuppressLint("SharedFlowCreation") class ZenModeRepositoryImpl( private val context: Context, private val notificationManager: NotificationManager, @@ -74,7 +77,7 @@ class ZenModeRepositoryImpl( val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - intent?.action?.let { action -> launch { send(action) } } + intent?.let { launch { send(it) } } } } @@ -99,12 +102,11 @@ class ZenModeRepositoryImpl( } .let { if (Flags.volumePanelBroadcastFix()) { + // Share the flow to avoid having multiple broadcasts. it.flowOn(backgroundCoroutineContext) + .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope) } else { - it.shareIn( - started = SharingStarted.WhileSubscribed(), - scope = scope, - ) + it.shareIn(started = SharingStarted.WhileSubscribed(), scope = scope) } } } @@ -112,7 +114,9 @@ class ZenModeRepositoryImpl( override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy { if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi()) flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) { - notificationManager.consolidatedNotificationPolicy + // If available, get the value from extras to avoid a potential binder call. + it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY) + ?: notificationManager.consolidatedNotificationPolicy } else flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) { @@ -126,11 +130,11 @@ class ZenModeRepositoryImpl( } } - private fun <T> flowFromBroadcast(intentAction: String, mapper: () -> T) = + private fun <T> flowFromBroadcast(intentAction: String, mapper: (Intent?) -> T) = notificationBroadcasts - .filter { intentAction == it } - .map { mapper() } - .onStart { emit(mapper()) } + .filter { intentAction == it.action } + .map { mapper(it) } + .onStart { emit(mapper(null)) } .flowOn(backgroundCoroutineContext) .stateIn(scope, SharingStarted.WhileSubscribed(), null) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt index 9dbf23eba7a5..eb33a7a10524 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt @@ -16,33 +16,84 @@ package com.android.settingslib.volume.data.repository +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothCsipSetCoordinator +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata +import android.bluetooth.BluetoothProfile +import android.bluetooth.BluetoothVolumeControl +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.provider.Settings +import androidx.annotation.IntRange import com.android.internal.util.ConcurrentUtils +import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.bluetooth.onProfileConnectionStateChanged +import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.flags.Flags +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +typealias GroupIdToVolumes = Map<Int, Int> /** Provides audio sharing functionality. */ interface AudioSharingRepository { /** Whether the device is in audio sharing. */ val inAudioSharing: Flow<Boolean> + + /** The secondary headset groupId in audio sharing. */ + val secondaryGroupId: StateFlow<Int> + + /** The headset groupId to volume map during audio sharing. */ + val volumeMap: StateFlow<GroupIdToVolumes> + + /** Set the volume of secondary headset during audio sharing. */ + suspend fun setSecondaryVolume( + @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) + volume: Int + ) + + companion object { + const val AUDIO_SHARING_VOLUME_MIN = 0 + const val AUDIO_SHARING_VOLUME_MAX = 255 + } } +@OptIn(ExperimentalCoroutinesApi::class) class AudioSharingRepositoryImpl( - private val localBluetoothManager: LocalBluetoothManager?, - backgroundCoroutineContext: CoroutineContext, + private val context: Context, + private val contentResolver: ContentResolver, + private val btManager: LocalBluetoothManager?, + private val coroutineScope: CoroutineScope, + private val backgroundCoroutineContext: CoroutineContext, ) : AudioSharingRepository { override val inAudioSharing: Flow<Boolean> = if (Flags.enableLeAudioSharing()) { - localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast -> + btManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast -> callbackFlow { val listener = object : BluetoothLeBroadcast.Callback { @@ -92,9 +143,117 @@ class AudioSharingRepositoryImpl( flowOf(false) } + private val primaryChange: Flow<Unit> = callbackFlow { + val callback = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + launch { send(Unit) } + } + } + contentResolver.registerContentObserver( + Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), + false, + callback) + awaitClose { contentResolver.unregisterContentObserver(callback) } + } + + override val secondaryGroupId: StateFlow<Int> = + if (Flags.volumeDialogAudioSharingFix()) { + merge( + btManager + ?.profileManager + ?.leAudioBroadcastAssistantProfile + ?.onSourceConnectedOrRemoved + ?.map { getSecondaryGroupId() } ?: emptyFlow(), + btManager + ?.eventManager + ?.onProfileConnectionStateChanged + ?.filter { profileConnection -> + profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED && + profileConnection.bluetoothProfile == + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + } + ?.map { getSecondaryGroupId() } ?: emptyFlow(), + primaryChange.map { getSecondaryGroupId() }) + .onStart { emit(getSecondaryGroupId()) } + .distinctUntilChanged() + .flowOn(backgroundCoroutineContext) + } else { + emptyFlow() + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId()) + + override val volumeMap: StateFlow<GroupIdToVolumes> = + if (Flags.volumeDialogAudioSharingFix()) { + btManager?.profileManager?.volumeControlProfile?.let { volumeControl -> + inAudioSharing.flatMapLatest { isSharing -> + if (isSharing) { + callbackFlow { + val callback = + object : BluetoothVolumeControl.Callback { + override fun onDeviceVolumeChanged( + device: BluetoothDevice, + @IntRange( + from = AUDIO_SHARING_VOLUME_MIN.toLong(), + to = AUDIO_SHARING_VOLUME_MAX.toLong()) + volume: Int + ) { + launch { send(Pair(device, volume)) } + } + } + // Once registered, we will receive the initial volume of all + // connected BT devices on VolumeControlProfile via callbacks + volumeControl.registerCallback( + ConcurrentUtils.DIRECT_EXECUTOR, callback) + awaitClose { volumeControl.unregisterCallback(callback) } + } + .runningFold(emptyMap<Int, Int>()) { acc, value -> + val groupId = + BluetoothUtils.getGroupId( + btManager.cachedDeviceManager?.findDevice(value.first)) + if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + acc + Pair(groupId, value.second) + } else { + acc + } + } + .distinctUntilChanged() + .flowOn(backgroundCoroutineContext) + } else { + emptyFlow() + } + } + } ?: emptyFlow() + } else { + emptyFlow() + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap()) + + override suspend fun setSecondaryVolume( + @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) + volume: Int + ) { + withContext(backgroundCoroutineContext) { + if (Flags.volumeDialogAudioSharingFix()) { + btManager?.profileManager?.volumeControlProfile?.let { + // Find secondary headset and set volume. + val cachedDevice = + BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager) + if (cachedDevice != null) { + it.setDeviceVolume(cachedDevice.device, volume, /* isGroupOp= */ true) + } + } + } + } + } + private fun isBroadcasting(): Boolean { return Flags.enableLeAudioSharing() && - (localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null) - ?: false) + (btManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null) ?: false) + } + + private fun getSecondaryGroupId(): Int { + return BluetoothUtils.getGroupId( + BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager)) } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt index 1c80ef4fe4d4..000664dd1552 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt @@ -16,15 +16,33 @@ package com.android.settingslib.volume.data.repository +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothLeBroadcast +import android.bluetooth.BluetoothLeBroadcastAssistant +import android.bluetooth.BluetoothLeBroadcastReceiveState +import android.bluetooth.BluetoothProfile +import android.bluetooth.BluetoothVolumeControl +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothCallback +import com.android.settingslib.bluetooth.BluetoothEventManager +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.settingslib.bluetooth.VolumeControlProfile import com.android.settingslib.flags.Flags import com.google.common.truth.Truth import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -39,6 +57,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.never @@ -52,27 +73,76 @@ import org.mockito.junit.MockitoRule @RunWith(AndroidJUnit4::class) class AudioSharingRepositoryTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() - @Mock private lateinit var localBluetoothManager: LocalBluetoothManager - @Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager - @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast + @Mock private lateinit var btManager: LocalBluetoothManager + + @Mock private lateinit var profileManager: LocalBluetoothProfileManager + + @Mock private lateinit var broadcast: LocalBluetoothLeBroadcast + + @Mock private lateinit var assistant: LocalBluetoothLeBroadcastAssistant + + @Mock private lateinit var volumeControl: VolumeControlProfile + + @Mock private lateinit var eventManager: BluetoothEventManager + + @Mock private lateinit var deviceManager: CachedBluetoothDeviceManager + + @Mock private lateinit var device1: BluetoothDevice + + @Mock private lateinit var device2: BluetoothDevice + + @Mock private lateinit var cachedDevice1: CachedBluetoothDevice + + @Mock private lateinit var cachedDevice2: CachedBluetoothDevice + + @Mock private lateinit var receiveState: BluetoothLeBroadcastReceiveState + + @Mock private lateinit var contentResolver: ContentResolver @Captor - private lateinit var leBroadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> - private val testScope = TestScope() + private lateinit var broadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> + + @Captor + private lateinit var assistantCallbackCaptor: + ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback> + + @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback> + + @Captor private lateinit var contentObserverCaptor: ArgumentCaptor<ContentObserver> + @Captor + private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback> + + private val testScope = TestScope() + private val context: Context = ApplicationProvider.getApplicationContext() private lateinit var underTest: AudioSharingRepository @Before fun setup() { - `when`(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager) - `when`(localBluetoothProfileManager.leAudioBroadcastProfile) - .thenReturn(localBluetoothLeBroadcast) - `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true) + `when`(btManager.profileManager).thenReturn(profileManager) + `when`(profileManager.leAudioBroadcastProfile).thenReturn(broadcast) + `when`(profileManager.leAudioBroadcastAssistantProfile).thenReturn(assistant) + `when`(profileManager.volumeControlProfile).thenReturn(volumeControl) + `when`(btManager.eventManager).thenReturn(eventManager) + `when`(btManager.cachedDeviceManager).thenReturn(deviceManager) + `when`(broadcast.isEnabled(null)).thenReturn(true) + `when`(cachedDevice1.groupId).thenReturn(TEST_GROUP_ID1) + `when`(cachedDevice1.device).thenReturn(device1) + `when`(deviceManager.findDevice(device1)).thenReturn(cachedDevice1) + `when`(cachedDevice2.groupId).thenReturn(TEST_GROUP_ID2) + `when`(cachedDevice2.device).thenReturn(device2) + `when`(deviceManager.findDevice(device2)).thenReturn(cachedDevice2) + `when`(receiveState.bisSyncState).thenReturn(arrayListOf(TEST_RECEIVE_STATE_CONTENT)) + `when`(assistant.getAllSources(any())).thenReturn(listOf(receiveState)) underTest = AudioSharingRepositoryImpl( - localBluetoothManager, + context, + contentResolver, + btManager, + testScope.backgroundScope, testScope.testScheduler, ) } @@ -84,9 +154,9 @@ class AudioSharingRepositoryTest { val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() - triggerAudioSharingStateChange(false) + triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped) runCurrent() - triggerAudioSharingStateChange(true) + triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted) runCurrent() Truth.assertThat(states).containsExactly(true, false, true) @@ -102,19 +172,229 @@ class AudioSharingRepositoryTest { runCurrent() Truth.assertThat(states).containsExactly(false) - verify(localBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any()) - verify(localBluetoothLeBroadcast, never()).isEnabled(any()) + verify(broadcast, never()).registerServiceCallBack(any(), any()) + verify(broadcast, never()).isEnabled(any()) + } + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun secondaryGroupIdChange_emitValues() { + testScope.runTest { + val groupIds = mutableListOf<Int?>() + underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) + runCurrent() + triggerSourceAdded() + runCurrent() + triggerContentObserverChange() + runCurrent() + triggerSourceRemoved() + runCurrent() + triggerSourceAdded() + runCurrent() + triggerProfileConnectionChange( + BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + runCurrent() + triggerProfileConnectionChange( + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO) + runCurrent() + triggerProfileConnectionChange( + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + runCurrent() + + Truth.assertThat(groupIds) + .containsExactly( + TEST_GROUP_ID_INVALID, + TEST_GROUP_ID2, + TEST_GROUP_ID1, + TEST_GROUP_ID_INVALID, + TEST_GROUP_ID2, + TEST_GROUP_ID_INVALID) } } - private fun triggerAudioSharingStateChange(inAudioSharing: Boolean) { - verify(localBluetoothLeBroadcast) - .registerServiceCallBack(any(), leBroadcastCallbackCaptor.capture()) - `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(inAudioSharing) - if (inAudioSharing) { - leBroadcastCallbackCaptor.value.onBroadcastStarted(0, 0) - } else { - leBroadcastCallbackCaptor.value.onBroadcastStopped(0, 0) + @Test + @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun secondaryGroupIdChange_audioSharingFlagOff_returnFalse() { + testScope.runTest { + val groupIds = mutableListOf<Int?>() + underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) + runCurrent() + + Truth.assertThat(groupIds).containsExactly(TEST_GROUP_ID_INVALID) + verify(assistant, never()).registerServiceCallBack(any(), any()) + verify(eventManager, never()).registerCallback(any()) + } + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun volumeMapChange_emitValues() { + testScope.runTest { + val volumeMaps = mutableListOf<GroupIdToVolumes?>() + underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) + runCurrent() + triggerVolumeMapChange(Pair(device1, TEST_VOLUME1)) + runCurrent() + triggerVolumeMapChange(Pair(device1, TEST_VOLUME2)) + runCurrent() + triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped) + runCurrent() + verify(volumeControl).unregisterCallback(any()) + runCurrent() + + Truth.assertThat(volumeMaps) + .containsExactly( + emptyMap<Int, Int>(), + mapOf(TEST_GROUP_ID1 to TEST_VOLUME1), + mapOf(TEST_GROUP_ID1 to TEST_VOLUME2)) + } + } + + @Test + @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun volumeMapChange_audioSharingFlagOff_returnFalse() { + testScope.runTest { + val volumeMaps = mutableListOf<GroupIdToVolumes?>() + underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) + runCurrent() + + Truth.assertThat(volumeMaps).isEmpty() + verify(broadcast, never()).registerServiceCallBack(any(), any()) + verify(volumeControl, never()).registerCallback(any(), any()) + } + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun setSecondaryVolume_setValue() { + testScope.runTest { + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID2) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) + underTest.setSecondaryVolume(TEST_VOLUME1) + + runCurrent() + verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true) + } + } + + @Test + @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + fun setSecondaryVolume_audioSharingFlagOff_doNothing() { + testScope.runTest { + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID2) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) + underTest.setSecondaryVolume(TEST_VOLUME1) + + runCurrent() + verify(volumeControl, never()).setDeviceVolume(any(), anyInt(), anyBoolean()) + } + } + + private fun triggerAudioSharingStateChange( + type: TriggerType, + broadcastAction: BluetoothLeBroadcast.Callback.() -> Unit + ) { + verify(broadcast).registerServiceCallBack(any(), broadcastCallbackCaptor.capture()) + when (type) { + TriggerType.BROADCAST_START -> { + `when`(broadcast.isEnabled(null)).thenReturn(true) + broadcastCallbackCaptor.value.broadcastAction() + } + TriggerType.BROADCAST_STOP -> { + `when`(broadcast.isEnabled(null)).thenReturn(false) + broadcastCallbackCaptor.value.broadcastAction() + } + } + } + + private fun triggerSourceAdded() { + verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture()) + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID1) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) + assistantCallbackCaptor.value.sourceAdded(device1, receiveState) + } + + private fun triggerSourceRemoved() { + verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture()) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1)) + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID1) + assistantCallbackCaptor.value.sourceRemoved(device2) + } + + private fun triggerProfileConnectionChange(state: Int, profile: Int) { + verify(eventManager).registerCallback(btCallbackCaptor.capture()) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1)) + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID1) + btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile) + } + + private fun triggerContentObserverChange() { + verify(contentResolver) + .registerContentObserver( + eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())), + eq(false), + contentObserverCaptor.capture()) + `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) + Settings.Secure.putInt( + context.contentResolver, + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + TEST_GROUP_ID2) + contentObserverCaptor.value.primaryChanged() + } + + private fun triggerVolumeMapChange(change: Pair<BluetoothDevice, Int>) { + verify(volumeControl).registerCallback(any(), volumeCallbackCaptor.capture()) + volumeCallbackCaptor.value.onDeviceVolumeChanged(change.first, change.second) + } + + private enum class TriggerType { + BROADCAST_START, + BROADCAST_STOP + } + + private companion object { + const val TEST_GROUP_ID_INVALID = -1 + const val TEST_GROUP_ID1 = 1 + const val TEST_GROUP_ID2 = 2 + const val TEST_SOURCE_ID = 1 + const val TEST_BROADCAST_ID = 1 + const val TEST_REASON = 1 + const val TEST_RECEIVE_STATE_CONTENT = 1L + const val TEST_VOLUME1 = 10 + const val TEST_VOLUME2 = 20 + + val broadcastStarted: BluetoothLeBroadcast.Callback.() -> Unit = { + onBroadcastStarted(TEST_REASON, TEST_BROADCAST_ID) + } + val broadcastStopped: BluetoothLeBroadcast.Callback.() -> Unit = { + onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID) } + val sourceAdded: + BluetoothLeBroadcastAssistant.Callback.( + sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState) -> Unit = + { sink, state -> + onReceiveStateChanged(sink, TEST_SOURCE_ID, state) + } + val sourceRemoved: BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit = + { sink -> + onSourceRemoved(sink, TEST_SOURCE_ID, TEST_REASON) + } + val primaryChanged: ContentObserver.() -> Unit = { onChange(false) } } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt index 6e11e1f612ef..67c73b1c8f5a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt @@ -22,8 +22,10 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.database.ContentObserver +import android.os.Parcelable import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings.Global import androidx.test.filters.SmallTest import com.android.settingslib.flags.Flags @@ -38,6 +40,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -54,6 +57,8 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @SmallTest class ZenModeRepositoryTest { + @get:Rule val setFlagsRule = SetFlagsRule() + @Mock private lateinit var context: Context @Mock private lateinit var notificationManager: NotificationManager @@ -126,6 +131,26 @@ class ZenModeRepositoryTest { } } + @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX) + @Test + fun consolidatedPolicyChanges_repositoryEmitsFromExtras() { + testScope.runTest { + val values = mutableListOf<NotificationManager.Policy?>() + `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1) + underTest.consolidatedNotificationPolicy + .onEach { values.add(it) } + .launchIn(backgroundScope) + runCurrent() + + triggerIntent( + NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED, + extras = mapOf(NotificationManager.EXTRA_NOTIFICATION_POLICY to testPolicy2)) + runCurrent() + + assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder() + } + } + @Test fun zenModeChanges_repositoryEmits() { testScope.runTest { @@ -174,9 +199,13 @@ class ZenModeRepositoryTest { } } - private fun triggerIntent(action: String) { + private fun triggerIntent(action: String, extras: Map<String, Parcelable>? = null) { verify(context).registerReceiver(receiverCaptor.capture(), any(), any(), any()) - receiverCaptor.value.onReceive(context, Intent(action)) + val intent = Intent(action) + if (extras?.isNotEmpty() == true) { + extras.forEach { (key, value) -> intent.putExtra(key, value) } + } + receiverCaptor.value.onReceive(context, intent) } private fun triggerZenModeSettingUpdate() { diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java index 8dd51b2b8d11..8de0c35ffa12 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java @@ -22,8 +22,6 @@ import static junit.framework.Assert.assertNull; import android.content.ContentResolver; import android.os.Bundle; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; @@ -73,24 +71,9 @@ public class DeviceConfigServiceTest { } /** - * Test that setting overrides are properly disabled when the flag is off. - */ - @Test - @RequiresFlagsDisabled("com.android.providers.settings.support_overrides") - public void testOverrideDisabled() throws IOException { - final String newValue = "value2"; - - executeShellCommand("device_config put " + sNamespace + " " + sKey + " " + sValue); - executeShellCommand("device_config override " + sNamespace + " " + sKey + " " + newValue); - String result = readShellCommandOutput("device_config get " + sNamespace + " " + sKey); - assertEquals(sValue + "\n", result); - } - - /** * Test that overrides are readable and can be cleared. */ @Test - @RequiresFlagsEnabled("com.android.providers.settings.support_overrides") public void testOverride() throws IOException { final String newValue = "value2"; diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 43d51c37aa36..92f03d792554 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -45,7 +45,7 @@ import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys -import com.android.systemui.communal.ui.compose.Dimensions.SlideOffsetY +import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffsetY import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 8f247f60bfc4..ef6eec88bf6d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -16,6 +16,8 @@ package com.android.systemui.communal.ui.compose +import android.content.Context +import android.content.res.Configuration import android.graphics.drawable.Icon import android.os.Bundle import android.util.SizeF @@ -40,6 +42,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -64,6 +67,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -114,6 +118,7 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag @@ -127,6 +132,7 @@ import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection @@ -386,6 +392,10 @@ fun CommunalHub( } } +val hubDimensions: Dimensions + @Composable + get() = Dimensions(LocalContext.current, LocalConfiguration.current, LocalDensity.current) + @Composable private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) { val colors = LocalAndroidColorScheme.current @@ -507,7 +517,6 @@ private fun BoxScope.CommunalHubLazyGrid( gridState = gridState, contentListState = contentListState, contentOffset = contentOffset, - updateDragPositionForRemove = updateDragPositionForRemove ) // A full size box in background that listens to widget drops from the picker. @@ -515,7 +524,7 @@ private fun BoxScope.CommunalHubLazyGrid( // for android drag events. Box(Modifier.fillMaxSize().dragAndDropTarget(dragAndDropTargetState)) {} } else { - gridModifier = gridModifier.height(Dimensions.GridHeight) + gridModifier = gridModifier.height(hubDimensions.GridHeight) } LazyHorizontalGrid( @@ -593,7 +602,7 @@ private fun EmptyStateCta( ) { val colors = LocalAndroidColorScheme.current Card( - modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding), + modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding), colors = CardDefaults.cardColors(containerColor = Color.Transparent), border = BorderStroke(3.dp, colors.secondary), shape = RoundedCornerShape(size = 80.dp) @@ -963,9 +972,25 @@ private fun WidgetContent( val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle() val selectedIndex = selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } } + + val isSelected = selectedKey == model.key + + val selectableModifier = + if (viewModel.isEditMode) { + Modifier.selectable( + selected = isSelected, + onClick = { viewModel.setSelectedKey(model.key) }, + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) + } else { + Modifier + } + Box( modifier = modifier + .then(selectableModifier) .thenIf(!viewModel.isEditMode && model.inQuietMode) { Modifier.pointerInput(Unit) { // consume tap to prevent the child view from triggering interactions with @@ -1262,7 +1287,7 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd return PaddingValues( start = Dimensions.ItemSpacing, end = Dimensions.ItemSpacing, - top = Dimensions.GridTopSpacing, + top = hubDimensions.GridTopSpacing, ) } val context = LocalContext.current @@ -1271,7 +1296,8 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd val screenHeight = with(density) { windowMetrics.bounds.height().toDp() } val toolbarHeight = with(density) { Dimensions.ToolbarPaddingTop + toolbarSize.height.toDp() } val verticalPadding = - ((screenHeight - toolbarHeight - Dimensions.GridHeight + Dimensions.GridTopSpacing) / 2) + ((screenHeight - toolbarHeight - hubDimensions.GridHeight + hubDimensions.GridTopSpacing) / + 2) .coerceAtLeast(Dimensions.Spacing) return PaddingValues( start = Dimensions.ToolbarPaddingHorizontal, @@ -1327,29 +1353,44 @@ data class ContentPaddingInPx(val start: Float, val top: Float) { fun toOffset(): Offset = Offset(start, top) } -object Dimensions { - val CardHeightFull = 530.dp - val GridTopSpacing = 114.dp +class Dimensions(val context: Context, val config: Configuration, val density: Density) { + val GridTopSpacing: Dp + get() { + if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { + return 114.dp + } else { + val windowMetrics = + WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) + val screenHeight = with(density) { windowMetrics.bounds.height().toDp() } + + return (screenHeight - CardHeightFull) / 2 + } + } + val GridHeight = CardHeightFull + GridTopSpacing - val ItemSpacing = 50.dp - val CardHeightHalf = (CardHeightFull - ItemSpacing) / 2 - val CardHeightThird = (CardHeightFull - (2 * ItemSpacing)) / 3 - val CardWidth = 360.dp - val CardOutlineWidth = 3.dp - val Spacing = ItemSpacing / 2 - - // The sizing/padding of the toolbar in glanceable hub edit mode - val ToolbarPaddingTop = 27.dp - val ToolbarPaddingHorizontal = ItemSpacing - val ToolbarButtonPaddingHorizontal = 24.dp - val ToolbarButtonPaddingVertical = 16.dp - val ButtonPadding = - PaddingValues( - vertical = ToolbarButtonPaddingVertical, - horizontal = ToolbarButtonPaddingHorizontal, - ) - val IconSize = 40.dp - val SlideOffsetY = 30.dp + + companion object { + val CardHeightFull = 530.dp + val ItemSpacing = 50.dp + val CardHeightHalf = (CardHeightFull - ItemSpacing) / 2 + val CardHeightThird = (CardHeightFull - (2 * ItemSpacing)) / 3 + val CardWidth = 360.dp + val CardOutlineWidth = 3.dp + val Spacing = ItemSpacing / 2 + + // The sizing/padding of the toolbar in glanceable hub edit mode + val ToolbarPaddingTop = 27.dp + val ToolbarPaddingHorizontal = ItemSpacing + val ToolbarButtonPaddingHorizontal = 24.dp + val ToolbarButtonPaddingVertical = 16.dp + val ButtonPadding = + PaddingValues( + vertical = ToolbarButtonPaddingVertical, + horizontal = ToolbarButtonPaddingHorizontal, + ) + val IconSize = 40.dp + val SlideOffsetY = 30.dp + } } private object Colors { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt index 9e6f22a69dbc..0c293948dd3c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt @@ -18,17 +18,13 @@ package com.android.systemui.communal.ui.compose import android.content.ClipDescription import android.view.DragEvent -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.lazy.grid.LazyGridItemInfo import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState @@ -45,8 +41,7 @@ import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.util.WidgetPickerIntentUtils import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch /** @@ -59,32 +54,22 @@ internal fun rememberDragAndDropTargetState( gridState: LazyGridState, contentOffset: Offset, contentListState: ContentListState, - updateDragPositionForRemove: (offset: Offset) -> Boolean, ): DragAndDropTargetState { val scope = rememberCoroutineScope() - val autoScrollSpeed = remember { mutableFloatStateOf(0f) } - // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value - // that allows differentiating intention of scrolling from intention of dragging over the first - // visible item. val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() } val state = - remember(gridState, contentListState) { + remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) { DragAndDropTargetState( state = gridState, contentOffset = contentOffset, contentListState = contentListState, - scope = scope, - autoScrollSpeed = autoScrollSpeed, autoScrollThreshold = autoScrollThreshold, - updateDragPositionForRemove = updateDragPositionForRemove, + scope = scope, ) } - LaunchedEffect(autoScrollSpeed.floatValue) { - if (autoScrollSpeed.floatValue != 0f) { - while (isActive) { - gridState.scrollBy(autoScrollSpeed.floatValue) - delay(10) - } + LaunchedEffect(state) { + for (diff in state.scrollChannel) { + gridState.scrollBy(diff) } } return state @@ -96,7 +81,6 @@ internal fun rememberDragAndDropTargetState( * @see androidx.compose.foundation.draganddrop.dragAndDropTarget * @see DragEvent */ -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun Modifier.dragAndDropTarget( dragDropTargetState: DragAndDropTargetState, @@ -122,6 +106,10 @@ internal fun Modifier.dragAndDropTarget( return state.onDrop(event) } + override fun onExited(event: DragAndDropEvent) { + state.onExited() + } + override fun onEnded(event: DragAndDropEvent) { state.onEnded() } @@ -149,19 +137,17 @@ internal class DragAndDropTargetState( private val state: LazyGridState, private val contentOffset: Offset, private val contentListState: ContentListState, - private val scope: CoroutineScope, - private val autoScrollSpeed: MutableState<Float>, private val autoScrollThreshold: Float, - private val updateDragPositionForRemove: (offset: Offset) -> Boolean, + private val scope: CoroutineScope, ) { /** * The placeholder item that is treated as if it is being dragged across the grid. It is added * to grid once drag and drop event is started and removed when event ends. */ private var placeHolder = CommunalContentModel.WidgetPlaceholder() - private var placeHolderIndex: Int? = null - private var isOnRemoveButton = false + + internal val scrollChannel = Channel<Float>() fun onStarted() { // assume item will be added to the end. @@ -170,39 +156,39 @@ internal class DragAndDropTargetState( } fun onMoved(event: DragAndDropEvent) { - val dragEvent = event.toAndroidDragEvent() - isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y)) - if (!isOnRemoveButton) { - findTargetItem(dragEvent)?.apply { - var scrollIndex: Int? = null - var scrollOffset: Int? = null - if (placeHolderIndex == state.firstVisibleItemIndex) { - // Save info about the first item before the move, to neutralize the automatic - // keeping first item first. - scrollIndex = placeHolderIndex - scrollOffset = state.firstVisibleItemScrollOffset - } + val dragOffset = event.toOffset() - autoScrollIfNearEdges(dragEvent) + val targetItem = + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .firstItemAtOffset(dragOffset - contentOffset) - if (contentListState.isItemEditable(this.index)) { - movePlaceholderTo(this.index) - placeHolderIndex = this.index - } + if (targetItem != null) { + var scrollIndex: Int? = null + var scrollOffset: Int? = null + if (placeHolderIndex == state.firstVisibleItemIndex) { + // Save info about the first item before the move, to neutralize the automatic + // keeping first item first. + scrollIndex = placeHolderIndex + scrollOffset = state.firstVisibleItemScrollOffset + } - if (scrollIndex != null && scrollOffset != null) { - // this is needed to neutralize automatic keeping the first item first. - scope.launch { state.scrollToItem(scrollIndex, scrollOffset) } - } + if (contentListState.isItemEditable(targetItem.index)) { + movePlaceholderTo(targetItem.index) + placeHolderIndex = targetItem.index } + + if (scrollIndex != null && scrollOffset != null) { + // this is needed to neutralize automatic keeping the first item first. + scope.launch { state.scrollToItem(scrollIndex, scrollOffset) } + } + } else { + computeAutoscroll(dragOffset).takeIf { it != 0f }?.let { scrollChannel.trySend(it) } } } fun onDrop(event: DragAndDropEvent): Boolean { - autoScrollSpeed.value = 0f - if (isOnRemoveButton) { - return false - } return placeHolderIndex?.let { dropIndex -> val widgetExtra = event.maybeWidgetExtra() ?: return false val (componentName, user) = widgetExtra @@ -221,39 +207,35 @@ internal class DragAndDropTargetState( } fun onEnded() { - autoScrollSpeed.value = 0f placeHolderIndex = null contentListState.list.remove(placeHolder) - isOnRemoveButton = updateDragPositionForRemove(Offset.Zero) } - private fun autoScrollIfNearEdges(dragEvent: DragEvent) { + fun onExited() { + onEnded() + } + + private fun computeAutoscroll(dragOffset: Offset): Float { val orientation = state.layoutInfo.orientation val distanceFromStart = if (orientation == Orientation.Horizontal) { - dragEvent.x + dragOffset.x } else { - dragEvent.y + dragOffset.y } val distanceFromEnd = if (orientation == Orientation.Horizontal) { - state.layoutInfo.viewportSize.width - dragEvent.x + state.layoutInfo.viewportEndOffset - dragOffset.x } else { - state.layoutInfo.viewportSize.height - dragEvent.y + state.layoutInfo.viewportEndOffset - dragOffset.y } - autoScrollSpeed.value = - when { - distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd - distanceFromStart < autoScrollThreshold -> - -(autoScrollThreshold - distanceFromStart) - else -> 0f - } - } - private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? = - state.layoutInfo.visibleItemsInfo.firstItemAtOffset( - Offset(dragEvent.x, dragEvent.y) - contentOffset - ) + return when { + distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd + distanceFromStart < autoScrollThreshold -> distanceFromStart - autoScrollThreshold + else -> 0f + } + } private fun movePlaceholderTo(index: Int) { val currentIndex = contentListState.list.indexOf(placeHolder) @@ -271,4 +253,6 @@ internal class DragAndDropTargetState( val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 } return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) } } + + private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt index 1ea73e144962..620892adc286 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt @@ -23,6 +23,8 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -39,11 +41,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -54,6 +61,7 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.PopupType import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.delay class CommunalPopupSection @Inject @@ -91,6 +99,17 @@ constructor( onClick: () -> Unit, onDismissRequest: () -> Unit, ) { + val interactionSource = remember { MutableInteractionSource() } + val focusRequester = remember { FocusRequester() } + + val context = LocalContext.current + + LaunchedEffect(Unit) { + // Adding a delay to ensure the animation completes before requesting focus + delay(250) + focusRequester.requestFocus() + } + Popup( alignment = Alignment.TopCenter, offset = IntOffset(0, 40), @@ -100,6 +119,8 @@ constructor( Button( modifier = Modifier.height(56.dp) + .focusRequester(focusRequester) + .focusable(interactionSource = interactionSource) .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) } .animateEnterExit( enter = @@ -142,8 +163,7 @@ constructor( ) { Icon( imageVector = Icons.Outlined.Widgets, - contentDescription = - stringResource(R.string.button_to_configure_widgets_text), + contentDescription = null, tint = colors.onSecondary, modifier = Modifier.size(20.dp) ) 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 a1f20423ad3b..859c0366a52f 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 @@ -90,10 +90,16 @@ constructor( */ @Composable fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) { + val areNotificationsVisible by + lockscreenContentViewModel + .areNotificationsVisible(sceneKey) + .collectAsStateWithLifecycle(initialValue = false) + if (!areNotificationsVisible) { + return + } + val isShadeLayoutWide by lockscreenContentViewModel.isShadeLayoutWide.collectAsStateWithLifecycle() - val areNotificationsVisible by - lockscreenContentViewModel.areNotificationsVisible.collectAsStateWithLifecycle() val splitShadeTopMargin: Dp = if (Flags.centralizedStatusBarHeightFix()) { LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp @@ -101,10 +107,6 @@ constructor( dimensionResource(id = R.dimen.large_screen_shade_header_height) } - if (!areNotificationsVisible) { - return - } - ConstrainedNotificationStack( stackScrollView = stackScrollView.get(), viewModel = viewModel, 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 a184cf3afb39..776e166f937c 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 @@ -198,6 +198,7 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( LaunchedEffect(scrollableState.isScrollInProgress) { if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) { + viewModel.setHeadsUpAnimatingAway(false) viewModel.snoozeHun() } } 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 index f9f7df820217..4f5d0e58ce01 100644 --- 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 @@ -29,6 +29,7 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter 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.FaceHelpMessageDebouncer import com.android.systemui.biometrics.data.repository.FaceSensorInfo import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository @@ -36,7 +37,7 @@ 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.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus @@ -75,15 +76,20 @@ class BouncerMessageViewModelTest : SysuiTestCase() { private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private lateinit var underTest: BouncerMessageViewModel + private val ignoreHelpMessageId = 1 @Before fun setUp() { kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true + overrideResource( + R.array.config_face_acquire_device_entry_ignorelist, + intArrayOf(ignoreHelpMessageId) + ) underTest = kosmos.bouncerMessageViewModel overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable") kosmos.fakeSystemPropertiesHelper.set( - DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, "not mainline reboot" ) } @@ -379,7 +385,15 @@ class BouncerMessageViewModelTest : SysuiTestCase() { runCurrent() kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( - HelpFaceAuthenticationStatus(1, "some helpful message") + HelpFaceAuthenticationStatus(0, "some helpful message", 0) + ) + runCurrent() + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus( + 0, + "some helpful message", + FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS + ) ) runCurrent() assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt index 0de036988337..2cbfa7dec59f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.communal +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor @@ -60,6 +62,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_COMMUNAL_HUB) class CommunalSceneStartableTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -98,7 +101,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test - fun keyguardGoesAway_forceBlankScene() = + fun keyguardGoesAway_whenLaunchingWidget_doNotForceBlankScene() = with(kosmos) { testScope.runTest { val scene by collectLastValue(communalSceneInteractor.currentScene) @@ -106,6 +109,27 @@ class CommunalSceneStartableTest : SysuiTestCase() { communalSceneInteractor.changeScene(CommunalScenes.Communal) assertThat(scene).isEqualTo(CommunalScenes.Communal) + communalSceneInteractor.setIsLaunchingWidget(true) + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + testScope = this + ) + + assertThat(scene).isEqualTo(CommunalScenes.Communal) + } + } + + @Test + fun keyguardGoesAway_whenNotLaunchingWidget_forceBlankScene() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalSceneInteractor.currentScene) + + communalSceneInteractor.changeScene(CommunalScenes.Communal) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + communalSceneInteractor.setIsLaunchingWidget(false) fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt index 0cd3fb28e299..d51d3567dd8d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt @@ -26,14 +26,23 @@ import androidx.core.util.component2 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.testKosmos +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.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock -import org.mockito.kotlin.notNull import org.mockito.kotlin.refEq import org.mockito.kotlin.verify @@ -41,6 +50,7 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class SmartspaceInteractionHandlerTest : SysuiTestCase() { private val activityStarter = mock<ActivityStarter>() + private val kosmos = testKosmos() private val testIntent = PendingIntent.getActivity( @@ -51,29 +61,43 @@ class SmartspaceInteractionHandlerTest : SysuiTestCase() { ) private val testResponse = RemoteResponse.fromPendingIntent(testIntent) - private val underTest: SmartspaceInteractionHandler by lazy { - SmartspaceInteractionHandler(activityStarter) + private lateinit var underTest: SmartspaceInteractionHandler + + @Before + fun setUp() { + with(kosmos) { + underTest = SmartspaceInteractionHandler(activityStarter, communalSceneInteractor) + } } @Test fun launchAnimatorIsUsedForSmartspaceView() { - val parent = FrameLayout(context) - val view = SmartspaceAppWidgetHostView(context) - parent.addView(view) - val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) + with(kosmos) { + testScope.runTest { + val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) + assertFalse(launching!!) - underTest.onInteraction(view, testIntent, testResponse) + val parent = FrameLayout(context) + val view = SmartspaceAppWidgetHostView(context) + parent.addView(view) + val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) - // Verify that we pass in a non-null animation controller - verify(activityStarter) - .startPendingIntentWithoutDismissing( - /* intent = */ eq(testIntent), - /* dismissShade = */ eq(false), - /* intentSentUiThreadCallback = */ isNull(), - /* animationController = */ notNull(), - /* fillInIntent = */ refEq(fillInIntent), - /* extraOptions = */ refEq(activityOptions.toBundle()), - ) + underTest.onInteraction(view, testIntent, testResponse) + + // Verify that we set the state correctly + assertTrue(launching!!) + // Verify that we pass in a non-null Communal animation controller + verify(activityStarter) + .startPendingIntentWithoutDismissing( + /* intent = */ eq(testIntent), + /* dismissShade = */ eq(false), + /* intentSentUiThreadCallback = */ isNull(), + /* animationController = */ any<CommunalTransitionAnimatorController>(), + /* fillInIntent = */ refEq(fillInIntent), + /* extraOptions = */ refEq(activityOptions.toBundle()), + ) + } + } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt new file mode 100644 index 000000000000..ac50db483301 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt @@ -0,0 +1,115 @@ +/* + * 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. + */ + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth +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.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalTransitionAnimatorControllerTest : SysuiTestCase() { + private val controller = mock<ActivityTransitionAnimator.Controller>() + private val kosmos = testKosmos() + + private lateinit var underTest: CommunalTransitionAnimatorController + + @Before + fun setUp() { + with(kosmos) { + underTest = CommunalTransitionAnimatorController(controller, communalSceneInteractor) + } + } + + @Test + fun doNotAnimate_launchingWidgetStateIsCleared() { + with(kosmos) { + testScope.runTest { + val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) + + communalSceneInteractor.setIsLaunchingWidget(true) + assertTrue(launching!!) + + underTest.onIntentStarted(willAnimate = false) + assertFalse(launching!!) + verify(controller).onIntentStarted(willAnimate = false) + } + } + } + + @Test + fun animationCancelled_launchingWidgetStateIsClearedAndSceneIsNotChanged() { + with(kosmos) { + testScope.runTest { + val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) + val scene by collectLastValue(communalSceneInteractor.currentScene) + + communalSceneInteractor.changeScene(CommunalScenes.Communal) + Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal) + communalSceneInteractor.setIsLaunchingWidget(true) + assertTrue(launching!!) + + underTest.onIntentStarted(willAnimate = true) + assertTrue(launching!!) + verify(controller).onIntentStarted(willAnimate = true) + + underTest.onTransitionAnimationCancelled(newKeyguardOccludedState = true) + assertFalse(launching!!) + Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal) + verify(controller).onTransitionAnimationCancelled(newKeyguardOccludedState = true) + } + } + } + + @Test + fun animationComplete_launchingWidgetStateIsClearedAndSceneIsChanged() { + with(kosmos) { + testScope.runTest { + val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) + val scene by collectLastValue(communalSceneInteractor.currentScene) + + communalSceneInteractor.changeScene(CommunalScenes.Communal) + Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal) + communalSceneInteractor.setIsLaunchingWidget(true) + assertTrue(launching!!) + + underTest.onIntentStarted(willAnimate = true) + assertTrue(launching!!) + verify(controller).onIntentStarted(willAnimate = true) + + underTest.onTransitionAnimationEnd(isExpandingFullyAbove = true) + assertFalse(launching!!) + Truth.assertThat(scene).isEqualTo(CommunalScenes.Blank) + verify(controller).onTransitionAnimationEnd(isExpandingFullyAbove = true) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt index 70448955eff0..ea8b5ab70cd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt @@ -26,13 +26,21 @@ import androidx.core.util.component2 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.testKosmos +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.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock -import org.mockito.kotlin.notNull import org.mockito.kotlin.refEq import org.mockito.kotlin.verify @@ -40,6 +48,7 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class WidgetInteractionHandlerTest : SysuiTestCase() { private val activityStarter = mock<ActivityStarter>() + private val kosmos = testKosmos() private val testIntent = PendingIntent.getActivity( @@ -50,30 +59,44 @@ class WidgetInteractionHandlerTest : SysuiTestCase() { ) private val testResponse = RemoteResponse.fromPendingIntent(testIntent) - private val underTest: WidgetInteractionHandler by lazy { - WidgetInteractionHandler(activityStarter) + private lateinit var underTest: WidgetInteractionHandler + + @Before + fun setUp() { + with(kosmos) { + underTest = WidgetInteractionHandler(activityStarter, communalSceneInteractor) + } } @Test fun launchAnimatorIsUsedForWidgetView() { - val parent = FrameLayout(context) - val view = CommunalAppWidgetHostView(context) - parent.addView(view) - val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) + with(kosmos) { + testScope.runTest { + val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) + assertFalse(launching!!) - underTest.onInteraction(view, testIntent, testResponse) + val parent = FrameLayout(context) + val view = CommunalAppWidgetHostView(context) + parent.addView(view) + val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) - // Verify that we pass in a non-null animation controller - verify(activityStarter) - .startPendingIntentMaybeDismissingKeyguard( - /* intent = */ eq(testIntent), - /* dismissShade = */ eq(false), - /* intentSentUiThreadCallback = */ isNull(), - /* animationController = */ notNull(), - /* fillInIntent = */ refEq(fillInIntent), - /* extraOptions = */ refEq(activityOptions.toBundle()), - /* customMessage */ isNull(), - ) + underTest.onInteraction(view, testIntent, testResponse) + + // Verify that we set the state correctly + assertTrue(launching!!) + // Verify that we pass in a non-null Communal animation controller + verify(activityStarter) + .startPendingIntentMaybeDismissingKeyguard( + /* intent = */ eq(testIntent), + /* dismissShade = */ eq(false), + /* intentSentUiThreadCallback = */ isNull(), + /* animationController = */ any<CommunalTransitionAnimatorController>(), + /* fillInIntent = */ refEq(fillInIntent), + /* extraOptions = */ refEq(activityOptions.toBundle()), + /* customMessage */ isNull(), + ) + } + } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 2546f27cb351..2bf50b3daa2f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -52,7 +52,6 @@ import com.android.systemui.deviceentry.shared.FaceAuthUiEvent.FACE_AUTH_TRIGGER import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus -import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.dump.DumpManager @@ -79,7 +78,6 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor -import com.android.systemui.res.R import com.android.systemui.statusbar.commandQueue import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.testKosmos @@ -478,29 +476,6 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun faceHelpMessagesAreIgnoredBasedOnConfig() = - testScope.runTest { - overrideResource( - R.array.config_face_acquire_device_entry_ignorelist, - intArrayOf(10, 11) - ) - underTest = createDeviceEntryFaceAuthRepositoryImpl() - initCollectors() - allPreconditionsToRunFaceAuthAreTrue() - - underTest.requestAuthenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER) - faceAuthenticateIsCalled() - - authenticationCallback.value.onAuthenticationHelp(9, "help msg") - authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg") - authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg") - - val response = authStatus() as HelpFaceAuthenticationStatus - assertThat(response.msg).isEqualTo("help msg") - assertThat(response.msgId).isEqualTo(response.msgId) - } - - @Test fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() = testScope.runTest { fakeUserRepository.setSelectedUserInfo(primaryUser) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 546510b2f144..3253edfb5fca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -20,7 +20,6 @@ import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey -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 @@ -32,27 +31,14 @@ import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.AdaptiveAuthRequest -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.BouncerLockedOut -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.PolicyLockdown -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.SecurityTimeout -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.TrustAgentDisabled -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UnattendedUpdate -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UserLockdown import com.android.systemui.flags.EnableSceneContainer -import com.android.systemui.flags.fakeSystemPropertiesHelper import com.android.systemui.keyguard.data.repository.biometricSettingsRepository 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.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository -import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope @@ -61,7 +47,6 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -438,231 +423,6 @@ class DeviceEntryInteractorTest : SysuiTestCase() { assertThat(isUnlocked).isTrue() } - @Test - fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() = - testScope.runTest { - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) - runCurrent() - - verifyRestrictionReasonsForAuthFlags( - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to null, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null, - LockPatternUtils.StrongAuthTracker - .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to - null, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - null, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to null - ) - } - - @Test - fun deviceEntryRestrictionReason_whenFaceIsEnrolledAndEnabled_mapsToAuthFlagsState() = - testScope.runTest { - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) - kosmos.fakeSystemPropertiesHelper.set( - DeviceEntryInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" - ) - runCurrent() - - verifyRestrictionReasonsForAuthFlags( - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to - DeviceNotUnlockedSinceReboot, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to - AdaptiveAuthRequest, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to - BouncerLockedOut, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to - SecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to - UserLockdown, - LockPatternUtils.StrongAuthTracker - .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to - NonStrongBiometricsSecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - UnattendedUpdate, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to - PolicyLockdown, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to - null, - ) - } - - @Test - fun deviceEntryRestrictionReason_whenFingerprintIsEnrolledAndEnabled_mapsToAuthFlagsState() = - testScope.runTest { - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) - kosmos.fakeSystemPropertiesHelper.set( - DeviceEntryInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" - ) - runCurrent() - - verifyRestrictionReasonsForAuthFlags( - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to - DeviceNotUnlockedSinceReboot, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to - AdaptiveAuthRequest, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to - BouncerLockedOut, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to - SecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to - UserLockdown, - LockPatternUtils.StrongAuthTracker - .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to - NonStrongBiometricsSecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - UnattendedUpdate, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to - PolicyLockdown, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to - null, - ) - } - - @Test - fun deviceEntryRestrictionReason_whenTrustAgentIsEnabled_mapsToAuthFlagsState() = - testScope.runTest { - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) - kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) - kosmos.fakeSystemPropertiesHelper.set( - DeviceEntryInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" - ) - runCurrent() - - verifyRestrictionReasonsForAuthFlags( - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to - DeviceNotUnlockedSinceReboot, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to - AdaptiveAuthRequest, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to - BouncerLockedOut, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to - SecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to - UserLockdown, - LockPatternUtils.StrongAuthTracker - .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to - NonStrongBiometricsSecurityTimeout, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to - UnattendedUpdate, - LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to - PolicyLockdown, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to - TrustAgentDisabled, - LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to - TrustAgentDisabled, - ) - } - - @Test - fun deviceEntryRestrictionReason_whenDeviceRebootedForMainlineUpdate_mapsToTheCorrectReason() = - testScope.runTest { - val deviceEntryRestrictionReason by - collectLastValue(underTest.deviceEntryRestrictionReason) - kosmos.fakeSystemPropertiesHelper.set( - DeviceEntryInteractor.SYS_BOOT_REASON_PROP, - DeviceEntryInteractor.REBOOT_MAINLINE_UPDATE - ) - kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( - AuthenticationFlags( - userId = 1, - flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT - ) - ) - runCurrent() - - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) - runCurrent() - - assertThat(deviceEntryRestrictionReason).isNull() - - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) - runCurrent() - - assertThat(deviceEntryRestrictionReason) - .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) - - kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) - runCurrent() - - assertThat(deviceEntryRestrictionReason) - .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) - - kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) - runCurrent() - - assertThat(deviceEntryRestrictionReason) - .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) - } - - @Test - fun reportUserPresent_whenDeviceEntered() = - testScope.runTest { - val isDeviceEntered by collectLastValue(underTest.isDeviceEntered) - assertThat(isDeviceEntered).isFalse() - assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(0) - - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - runCurrent() - switchToScene(Scenes.Gone) - assertThat(isDeviceEntered).isTrue() - assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(1) - - switchToScene(Scenes.Lockscreen) - assertThat(isDeviceEntered).isFalse() - assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(1) - - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - switchToScene(Scenes.Gone) - assertThat(isDeviceEntered).isTrue() - assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(2) - } - - private fun TestScope.verifyRestrictionReasonsForAuthFlags( - vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?> - ) { - val deviceEntryRestrictionReason by collectLastValue(underTest.deviceEntryRestrictionReason) - - authFlagToDeviceEntryRestriction.forEach { (flag, expectedReason) -> - kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( - AuthenticationFlags(userId = 1, flag = flag) - ) - runCurrent() - - if (expectedReason == null) { - assertThat(deviceEntryRestrictionReason).isNull() - } else { - assertThat(deviceEntryRestrictionReason).isEqualTo(expectedReason) - } - } - } - private fun switchToScene(sceneKey: SceneKey) { sceneInteractor.changeScene(sceneKey, "reason") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index a7a7bea313fe..c2acc5ff6689 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -19,17 +19,20 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.pm.UserInfo 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.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource +import com.android.systemui.flags.fakeSystemPropertiesHelper +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.domain.interactor.trustInteractor +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -40,6 +43,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -54,18 +58,8 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val authenticationRepository = kosmos.fakeAuthenticationRepository - private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository - - val underTest = - DeviceUnlockedInteractor( - applicationScope = testScope.backgroundScope, - authenticationInteractor = kosmos.authenticationInteractor, - deviceEntryRepository = deviceEntryRepository, - trustInteractor = kosmos.trustInteractor, - faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor, - fingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor, - powerInteractor = kosmos.powerInteractor, - ) + + val underTest = kosmos.deviceUnlockedInteractor @Before fun setup() { @@ -100,6 +94,30 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { } @Test + fun deviceUnlockStatus_whenUnlockedAndAuthMethodIsPinAndInLockdown_isFalse() = + testScope.runTest { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + val isInLockdown by collectLastValue(underTest.isInLockdown) + + authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags( + userId = 1, + flag = + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, + ) + ) + runCurrent() + assertThat(isInLockdown).isTrue() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull() + } + + @Test fun deviceUnlockStatus_whenUnlockedAndAuthMethodIsSim_isFalse() = testScope.runTest { val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) @@ -221,6 +239,218 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { assertThat(deviceUnlockStatus?.isUnlocked).isFalse() } + @Test + fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + null, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_whenLockdown() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + DeviceEntryRestrictionReason.UserLockdown, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + DeviceEntryRestrictionReason.PolicyLockdown + ) + } + + @Test + fun deviceEntryRestrictionReason_whenFaceIsEnrolledAndEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + DeviceEntryRestrictionReason.AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + DeviceEntryRestrictionReason.BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + DeviceEntryRestrictionReason.SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + DeviceEntryRestrictionReason.UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + DeviceEntryRestrictionReason.UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + DeviceEntryRestrictionReason.PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenFingerprintIsEnrolledAndEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + DeviceEntryRestrictionReason.AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + DeviceEntryRestrictionReason.BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + DeviceEntryRestrictionReason.SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + DeviceEntryRestrictionReason.UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + DeviceEntryRestrictionReason.UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + DeviceEntryRestrictionReason.PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + null, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenTrustAgentIsEnabled_mapsToAuthFlagsState() = + testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeSystemPropertiesHelper.set( + DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + runCurrent() + + verifyRestrictionReasonsForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to + DeviceEntryRestrictionReason.AdaptiveAuthRequest, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to + DeviceEntryRestrictionReason.BouncerLockedOut, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + DeviceEntryRestrictionReason.SecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + DeviceEntryRestrictionReason.UserLockdown, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + DeviceEntryRestrictionReason.UnattendedUpdate, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + DeviceEntryRestrictionReason.PolicyLockdown, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + DeviceEntryRestrictionReason.TrustAgentDisabled, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + DeviceEntryRestrictionReason.TrustAgentDisabled, + ) + } + + @Test + fun deviceEntryRestrictionReason_whenDeviceRebootedForMainlineUpdate_mapsToTheCorrectReason() = + testScope.runTest { + val deviceEntryRestrictionReason by + collectLastValue(underTest.deviceEntryRestrictionReason) + kosmos.fakeSystemPropertiesHelper.set( + DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, + DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE + ) + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags( + userId = 1, + flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT + ) + ) + runCurrent() + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) + runCurrent() + + assertThat(deviceEntryRestrictionReason).isNull() + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + runCurrent() + + assertThat(deviceEntryRestrictionReason) + .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) + } + + private fun TestScope.verifyRestrictionReasonsForAuthFlags( + vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?> + ) { + val deviceEntryRestrictionReason by collectLastValue(underTest.deviceEntryRestrictionReason) + + authFlagToDeviceEntryRestriction.forEach { (flag, expectedReason) -> + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(userId = 1, flag = flag) + ) + runCurrent() + + if (expectedReason == null) { + assertThat(deviceEntryRestrictionReason).isNull() + } else { + assertThat(deviceEntryRestrictionReason).isEqualTo(expectedReason) + } + } + } + companion object { private const val primaryUserId = 1 private val primaryUser = UserInfo(primaryUserId, "test user", UserInfo.FLAG_PRIMARY) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 444f63afb021..60c9bb03b68e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -41,6 +41,7 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent @@ -118,11 +119,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mDreamComplicationComponentFactory: - com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory + com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory @Mock lateinit var mDreamComplicationComponent: - com.android.systemui.dreams.complication.dagger.ComplicationComponent + com.android.systemui.dreams.complication.dagger.ComplicationComponent @Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler @@ -202,8 +203,12 @@ class DreamOverlayServiceTest : SysuiTestCase() { whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController) whenever(mLazyViewCapture.value).thenReturn(viewCaptureSpy) mWindowParams = WindowManager.LayoutParams() - mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mWindowManager, - mLazyViewCapture, isViewCaptureEnabled = false) + mViewCaptureAwareWindowManager = + ViewCaptureAwareWindowManager( + mWindowManager, + mLazyViewCapture, + isViewCaptureEnabled = false + ) mService = DreamOverlayService( mContext, @@ -257,7 +262,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) verify(mUiEventLogger) - .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) + .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) } @Test @@ -634,7 +639,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT) + @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB) @kotlin.Throws(RemoteException::class) fun testTransitionToGlanceableHub() = testScope.runTest { @@ -658,7 +663,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT) + @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB) @Throws(RemoteException::class) fun testRedirectExit() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt index 4a5342ac6ae1..3a4b14b81e07 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -28,6 +28,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.shared.education.GestureType.BACK_GESTURE import com.google.common.truth.Truth.assertThat import java.io.File +import java.time.Clock +import java.time.Instant import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestScope @@ -48,6 +50,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { private val dsScopeProvider: Provider<CoroutineScope> = Provider { TestScope(kosmos.testDispatcher).backgroundScope } + private val clock: Clock = FakeEduClock(Instant.ofEpochMilli(1000)) private val testUserId = 1111 // For deleting any test files created after the test @@ -59,7 +62,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { // needed before calling TemporaryFolder.newFolder(). val testContext = TestContext(context, tmpFolder.newFolder()) val userRepository = UserContextualEducationRepository(testContext, dsScopeProvider) - underTest = ContextualEducationRepository(userRepository) + underTest = ContextualEducationRepositoryImpl(clock, userRepository) underTest.setUser(testUserId) } @@ -85,6 +88,15 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { assertThat(model?.signalCount).isEqualTo(1) } + @Test + fun dataAddedOnUpdateShortcutTriggerTime() = + testScope.runTest { + val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE)) + assertThat(model?.lastShortcutTriggeredTime).isNull() + underTest.updateShortcutTriggerTime(BACK_GESTURE) + assertThat(model?.lastShortcutTriggeredTime).isEqualTo(clock.instant()) + } + /** Test context which allows overriding getFilesDir path */ private class TestContext(context: Context, private val folder: File) : SysuiTestableContext(context) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index ec4fd79b399a..b885800ab964 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -123,7 +123,7 @@ class FromDozingTransitionInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, Flags.FLAG_COMMUNAL_HUB) fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() = testScope.runTest { whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 50772eedc914..3075c54fb069 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -37,6 +38,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.whenever @@ -124,7 +126,50 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa fun areNotificationsVisible_splitShadeTrue_true() = with(kosmos) { testScope.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible) + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen)) + shadeRepository.setShadeLayoutWide(true) + fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) + + assertThat(areNotificationsVisible).isTrue() + } + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun areNotificationsVisible_dualShadeWideOnLockscreen_true() = + with(kosmos) { + testScope.runTest { + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen)) + shadeRepository.setShadeLayoutWide(true) + fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) + + assertThat(areNotificationsVisible).isTrue() + } + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun areNotificationsVisible_dualShadeWideOnNotificationsShade_false() = + with(kosmos) { + testScope.runTest { + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.NotificationsShade)) + shadeRepository.setShadeLayoutWide(true) + fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) + + assertThat(areNotificationsVisible).isFalse() + } + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun areNotificationsVisible_dualShadeWideOnQuickSettingsShade_true() = + with(kosmos) { + testScope.runTest { + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.QuickSettingsShade)) shadeRepository.setShadeLayoutWide(true) fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) @@ -137,7 +182,8 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa fun areNotificationsVisible_withSmallClock_true() = with(kosmos) { testScope.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible) + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen)) fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL) assertThat(areNotificationsVisible).isTrue() } @@ -148,7 +194,8 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa fun areNotificationsVisible_withLargeClock_false() = with(kosmos) { testScope.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible) + val areNotificationsVisible by + collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen)) fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) assertThat(areNotificationsVisible).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt index 1c3021ef5839..73a00395606e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt @@ -82,6 +82,20 @@ class DragAndDropStateTest : SysuiTestCase() { TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() } } + @Test + fun onMoveOutOfBounds_removeMovingTileFromCurrentList() { + val movingTileSpec = TestEditTiles[0].tileSpec + + // Start the drag movement + underTest.onStarted(movingTileSpec) + + // Move the tile outside of the list + underTest.movedOutOfBounds() + + // Asserts the moving tile is not current + assertThat(listState.tiles.first { it.tileSpec == movingTileSpec }.isCurrent).isFalse() + } + companion object { private fun createEditTile(tileSpec: String): EditTileViewModel { return EditTileViewModel( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt index ccd78ee82169..59e8ea63ecee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.assist.AssistManager import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController import com.android.systemui.shade.data.repository.FakeShadeRepository @@ -136,7 +137,9 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { communalSceneInteractor = communalSceneInteractor, ) `when`(userTracker.userHandle).thenReturn(UserHandle.OWNER) + `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(false)) `when`(communalSceneInteractor.isIdleOnCommunal).thenReturn(MutableStateFlow(false)) + `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(false)) } @Test @@ -335,6 +338,102 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) } + @EnableFlags(Flags.FLAG_COMMUNAL_HUB) + @Test + fun startPendingIntentDismissingKeyguard_transitionAnimator_animateCommunal() { + val parent = FrameLayout(context) + val view = + object : View(context), LaunchableView { + override fun setShouldBlockVisibilityChanges(block: Boolean) {} + } + parent.addView(view) + val controller = ActivityTransitionAnimator.Controller.fromView(view) + val pendingIntent = mock(PendingIntent::class.java) + `when`(pendingIntent.isActivity).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) + `when`(keyguardStateController.isOccluded).thenReturn(true) + `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true)) + `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(true)) + `when`(activityIntentHelper.wouldPendingLaunchResolverActivity(eq(pendingIntent), anyInt())) + .thenReturn(false) + `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt())) + .thenReturn(false) + + underTest.startPendingIntentDismissingKeyguard( + intent = pendingIntent, + dismissShade = false, + animationController = controller, + showOverLockscreen = true, + skipLockscreenChecks = false + ) + mainExecutor.runAllReady() + + val actionCaptor = argumentCaptor<OnDismissAction>() + verify(statusBarKeyguardViewManager) + .dismissWithAction(actionCaptor.capture(), eq(null), anyBoolean(), eq(null)) + actionCaptor.firstValue.onDismiss() + mainExecutor.runAllReady() + + verify(activityTransitionAnimator) + .startPendingIntentWithAnimation( + nullable(ActivityTransitionAnimator.Controller::class.java), + eq(true), + nullable(String::class.java), + eq(false), + any(), + ) + } + + @DisableFlags(Flags.FLAG_COMMUNAL_HUB) + @Test + fun startPendingIntentDismissingKeyguard_transitionAnimator_doNotAnimateCommunal() { + val parent = FrameLayout(context) + val view = + object : View(context), LaunchableView { + override fun setShouldBlockVisibilityChanges(block: Boolean) {} + } + parent.addView(view) + val controller = ActivityTransitionAnimator.Controller.fromView(view) + val pendingIntent = mock(PendingIntent::class.java) + `when`(pendingIntent.isActivity).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) + `when`(keyguardStateController.isOccluded).thenReturn(true) + `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true)) + `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(true)) + `when`(activityIntentHelper.wouldPendingLaunchResolverActivity(eq(pendingIntent), anyInt())) + .thenReturn(false) + `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt())) + .thenReturn(false) + + underTest.startPendingIntentDismissingKeyguard( + intent = pendingIntent, + dismissShade = false, + animationController = controller, + showOverLockscreen = true, + skipLockscreenChecks = false + ) + mainExecutor.runAllReady() + + val actionCaptor = argumentCaptor<OnDismissAction>() + verify(statusBarKeyguardViewManager) + .dismissWithAction(actionCaptor.capture(), eq(null), anyBoolean(), eq(null)) + actionCaptor.firstValue.onDismiss() + mainExecutor.runAllReady() + + val runnableCaptor = argumentCaptor<Runnable>() + verify(statusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(runnableCaptor.capture()) + runnableCaptor.firstValue.run() + + verify(activityTransitionAnimator) + .startPendingIntentWithAnimation( + nullable(ActivityTransitionAnimator.Controller::class.java), + eq(false), + nullable(String::class.java), + eq(false), + any(), + ) + } + @Test fun startActivity_noUserHandleProvided_getUserHandle() { val intent = mock(Intent::class.java) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt new file mode 100644 index 000000000000..142631e6aa07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt @@ -0,0 +1,100 @@ +/* + * 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.volume.domain.interactor + +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.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.audioSharingRepository +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class AudioSharingInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + lateinit var underTest: AudioSharingInteractor + + @Before + fun setUp() { + with(kosmos) { underTest = audioSharingInteractor } + } + + @Test + fun volumeChanges_returnVolume() { + with(kosmos) { + testScope.runTest { + with(audioSharingRepository) { + setSecondaryGroupId(TEST_GROUP_ID) + setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME)) + } + val volume by collectLastValue(underTest.volume) + runCurrent() + + Truth.assertThat(volume).isEqualTo(TEST_VOLUME) + } + } + } + + @Test + fun volumeChanges_returnNull() { + with(kosmos) { + testScope.runTest { + with(audioSharingRepository) { + setSecondaryGroupId(TEST_GROUP_ID_INVALID) + setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME)) + } + val volume by collectLastValue(underTest.volume) + runCurrent() + + Truth.assertThat(volume).isNull() + } + } + } + + @Test + fun volumeChanges_returnDefaultVolume() { + with(kosmos) { + testScope.runTest { + with(audioSharingRepository) { + setSecondaryGroupId(TEST_GROUP_ID) + setVolumeMap(emptyMap()) + } + val volume by collectLastValue(underTest.volume) + runCurrent() + + Truth.assertThat(volume).isEqualTo(TEST_VOLUME_DEFAULT) + } + } + } + + private companion object { + const val TEST_GROUP_ID = 1 + const val TEST_GROUP_ID_INVALID = -1 + const val TEST_VOLUME = 10 + const val TEST_VOLUME_DEFAULT = 20 + } +} diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7f7e6347b2f7..30f23bfc9753 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -287,7 +287,8 @@ <integer name="doze_small_icon_alpha">222</integer><!-- 87% of 0xff --> <!-- Doze: Table that translates sensor values from the doze_brightness_sensor_type sensor - to brightness values; -1 means keeping the current brightness. --> + to brightness values in the integer scale [1, 255]; -1 means keeping the current + brightness. --> <integer-array name="config_doze_brightness_sensor_to_brightness"> <item>-1</item> <!-- 0: OFF --> <item>2</item> <!-- 1: NIGHT --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index 090033d41ffa..5d804cc22b39 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -120,7 +120,7 @@ interface ISystemUiProxy { oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48; /** - * Notifies SystemUI to invoke IME Switcher. + * Notifies that the IME switcher button has been pressed. */ oneway void onImeSwitcherPressed() = 49; @@ -167,5 +167,10 @@ interface ISystemUiProxy { */ oneway void toggleQuickSettingsPanel() = 56; - // Next id = 57 + /** + * Notifies that the IME Switcher button has been long pressed. + */ + oneway void onImeSwitcherLongPress() = 57; + + // Next id = 58 } diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java index 56273eb9a2cf..6e257442d139 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java @@ -33,6 +33,7 @@ import android.view.WindowManager; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.assist.AssistLogger; @@ -66,7 +67,7 @@ public class DefaultUiController implements AssistManager.UiController { protected InvocationLightsView mInvocationLightsView; protected final AssistLogger mAssistLogger; - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; private final MetricsLogger mMetricsLogger; private final Lazy<AssistManager> mAssistManagerLazy; private final WindowManager.LayoutParams mLayoutParams; @@ -80,12 +81,12 @@ public class DefaultUiController implements AssistManager.UiController { @Inject public DefaultUiController(Context context, AssistLogger assistLogger, - WindowManager windowManager, MetricsLogger metricsLogger, - Lazy<AssistManager> assistManagerLazy, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + MetricsLogger metricsLogger, Lazy<AssistManager> assistManagerLazy, NavigationBarController navigationBarController) { mAssistLogger = assistLogger; mRoot = new FrameLayout(context); - mWindowManager = windowManager; + mWindowManager = viewCaptureAwareWindowManager; mMetricsLogger = metricsLogger; mAssistManagerLazy = assistManagerLazy; diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index a9f985f0955b..468737d9372f 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -288,6 +288,7 @@ constructor( override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) { withContext(backgroundDispatcher) { if (isSuccessful) { + lockPatternUtils.userPresent(selectedUserId) lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId) _hasLockoutOccurred.value = false } else { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt new file mode 100644 index 000000000000..1685f49e4f3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt @@ -0,0 +1,125 @@ +/* + * 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.biometrics + +import android.util.Log +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus + +/** + * Debounces face help messages with parameters: + * - window: Window of time (in milliseconds) to analyze face acquired messages) + * - startWindow: Window of time on start required before showing the first help message + * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to + * the user + */ +class FaceHelpMessageDebouncer( + private val window: Long = DEFAULT_WINDOW_MS, + private val startWindow: Long = window, + private val shownFaceMessageFrequencyBoost: Int = 4, +) { + private val TAG = "FaceHelpMessageDebouncer" + private var startTime = 0L + private var helpFaceAuthStatuses: MutableList<HelpFaceAuthenticationStatus> = mutableListOf() + private var lastMessageIdShown: Int? = null + + /** Remove messages that are outside of the time [window]. */ + private fun removeOldMessages(currTimestamp: Long) { + var numToRemove = 0 + // This works under the assumption that timestamps are ordered from first to last + // in chronological order + for (index in helpFaceAuthStatuses.indices) { + if ((helpFaceAuthStatuses[index].createdAt + window) >= currTimestamp) { + break // all timestamps from here and on are within the window + } + numToRemove += 1 + } + + // Remove all outside time window + repeat(numToRemove) { helpFaceAuthStatuses.removeFirst() } + + if (numToRemove > 0) { + Log.v(TAG, "removedFirst=$numToRemove") + } + } + + private fun getMostFrequentHelpMessage(): HelpFaceAuthenticationStatus? { + // freqMap: msgId => frequency + val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap() + + // Give shownFaceMessageFrequencyBoost to lastMessageIdShown + if (lastMessageIdShown != null) { + freqMap.computeIfPresent(lastMessageIdShown!!) { _, value -> + value + shownFaceMessageFrequencyBoost + } + } + // Go through all msgId keys & find the highest frequency msgId + val msgIdWithHighestFrequency = + freqMap.entries + .maxWithOrNull { (msgId1, freq1), (msgId2, freq2) -> + // ties are broken by more recent message + if (freq1 == freq2) { + helpFaceAuthStatuses + .findLast { it.msgId == msgId1 }!! + .createdAt + .compareTo( + helpFaceAuthStatuses.findLast { it.msgId == msgId2 }!!.createdAt + ) + } else { + freq1.compareTo(freq2) + } + } + ?.key + return helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } + } + + fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) { + helpFaceAuthStatuses.add(helpFaceAuthStatus) + Log.v(TAG, "added message=$helpFaceAuthStatus") + } + + fun getMessageToShow(atTimestamp: Long): HelpFaceAuthenticationStatus? { + if (helpFaceAuthStatuses.isEmpty() || (atTimestamp - startTime) < startWindow) { + // there's not enough time that has passed to determine whether to show anything yet + Log.v(TAG, "No message; haven't made initial threshold window OR no messages") + return null + } + removeOldMessages(atTimestamp) + val messageToShow = getMostFrequentHelpMessage() + if (lastMessageIdShown != messageToShow?.msgId) { + Log.v( + TAG, + "showMessage previousLastMessageId=$lastMessageIdShown" + + "\n\tmessageToShow=$messageToShow " + + "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" + + "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + ) + lastMessageIdShown = messageToShow?.msgId + } + return messageToShow + } + + fun startNewFaceAuthSession(faceAuthStartedTime: Long) { + Log.d(TAG, "startNewFaceAuthSession at startTime=$startTime") + startTime = faceAuthStartedTime + helpFaceAuthStatuses.clear() + lastMessageIdShown = null + } + + companion object { + const val DEFAULT_WINDOW_MS = 200L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index c868d01de743..430887dfcab6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -94,8 +94,16 @@ object BiometricViewBinder { val textColorError = view.resources.getColor(R.color.biometric_dialog_error, view.context.theme) + + val attributes = + view.context.obtainStyledAttributes( + R.style.TextAppearance_AuthCredential_Indicator, + intArrayOf(android.R.attr.textColor) + ) val textColorHint = - view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + if (constraintBp()) attributes.getColor(0, 0) + else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + attributes.recycle() val logoView = view.requireViewById<ImageView>(R.id.logo) val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description) 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 index 6cb9b16e2f9b..810b6d10f355 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -32,7 +32,7 @@ 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.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason import com.android.systemui.deviceentry.shared.model.FaceFailureMessage import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage @@ -75,7 +75,7 @@ class BouncerMessageViewModel( private val clock: SystemClock, private val biometricMessageInteractor: BiometricMessageInteractor, private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, - private val deviceEntryInteractor: DeviceEntryInteractor, + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, flags: ComposeBouncerFlags, ) { @@ -119,7 +119,7 @@ class BouncerMessageViewModel( } } else if (authMethod.isSecure) { combine( - deviceEntryInteractor.deviceEntryRestrictionReason, + deviceUnlockedInteractor.deviceEntryRestrictionReason, lockoutMessage, fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, resetToDefault, @@ -413,7 +413,7 @@ object BouncerMessageViewModelModule { clock: SystemClock, biometricMessageInteractor: BiometricMessageInteractor, faceAuthInteractor: DeviceEntryFaceAuthInteractor, - deviceEntryInteractor: DeviceEntryInteractor, + deviceUnlockedInteractor: DeviceUnlockedInteractor, fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, flags: ComposeBouncerFlags, userSwitcherViewModel: UserSwitcherViewModel, @@ -427,7 +427,7 @@ object BouncerMessageViewModelModule { clock = clock, biometricMessageInteractor = biometricMessageInteractor, faceAuthInteractor = faceAuthInteractor, - deviceEntryInteractor = deviceEntryInteractor, + deviceUnlockedInteractor = deviceUnlockedInteractor, fingerprintInteractor = fingerprintInteractor, flags = flags, selectedUser = userSwitcherViewModel.selectedUser, diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index bde6f42b16af..6b58c07b1706 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -105,7 +105,13 @@ constructor( .mapLatest(::determineSceneAfterTransition) .filterNotNull() .onEach { (nextScene, nextTransition) -> - communalSceneInteractor.changeScene(nextScene, nextTransition) + if (!communalSceneInteractor.isLaunchingWidget.value) { + // When launching a widget, we don't want to animate the scene change or the + // Communal Hub will reveal the wallpaper even though it shouldn't. Instead we + // snap to the new scene as part of the launch animation, once the activity + // launch is done, so we don't change scene here. + communalSceneInteractor.changeScene(nextScene, nextTransition) + } } .launchIn(applicationScope) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index e3ef6bbfad61..748c4fab5ee8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -20,6 +20,7 @@ import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL import android.content.IntentFilter import android.content.pm.UserInfo +import android.os.UserHandle import android.provider.Settings import com.android.systemui.Flags.communalHub import com.android.systemui.broadcast.BroadcastDispatcher @@ -102,7 +103,10 @@ constructor( .broadcastFlow( filter = IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - user = user.userHandle + // In COPE management mode, the restriction from the managed profile may + // propagate to the main profile. Therefore listen to this broadcast across + // all users and update the state each time it changes. + user = UserHandle.ALL, ) .emitOnStart() .map { devicePolicyManager.areKeyguardWidgetsAllowed(user.id) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index fd540c4b6708..122f964713a9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -48,6 +48,15 @@ constructor( @Application private val applicationScope: CoroutineScope, private val communalSceneRepository: CommunalSceneRepository, ) { + val _isLaunchingWidget = MutableStateFlow(false) + + /** Whether a widget launch is currently in progress. */ + val isLaunchingWidget: StateFlow<Boolean> = _isLaunchingWidget.asStateFlow() + + fun setIsLaunchingWidget(launching: Boolean) { + _isLaunchingWidget.value = launching + } + /** * Asks for an asynchronous scene witch to [newScene], which will use the corresponding * installed transition or the one specified by [transitionKey], if provided. diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt index a88b777be785..4e3d3ff8a265 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt @@ -22,21 +22,25 @@ import android.content.Intent import android.view.View import android.widget.RemoteViews import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.util.InteractionHandlerDelegate import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView import com.android.systemui.plugins.ActivityStarter import javax.inject.Inject -/** - * Handles interactions on smartspace elements on the hub. - */ -class SmartspaceInteractionHandler @Inject constructor( +/** Handles interactions on smartspace elements on the hub. */ +class SmartspaceInteractionHandler +@Inject +constructor( private val activityStarter: ActivityStarter, + communalSceneInteractor: CommunalSceneInteractor, ) : RemoteViews.InteractionHandler { - private val delegate = InteractionHandlerDelegate( - findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView }, - intentStarter = this::startIntent, - ) + private val delegate = + InteractionHandlerDelegate( + communalSceneInteractor, + findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView }, + intentStarter = this::startIntent, + ) override fun onInteraction( view: View, diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt index 40b182dba817..51a5fcd6b873 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt @@ -24,17 +24,17 @@ import android.widget.RemoteViews import androidx.core.util.component1 import androidx.core.util.component2 import com.android.systemui.animation.ActivityTransitionAnimator - +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController /** A delegate that can be used to launch activities from [RemoteViews] */ class InteractionHandlerDelegate( + private val communalSceneInteractor: CommunalSceneInteractor, private val findViewToAnimate: (View) -> Boolean, private val intentStarter: IntentStarter, ) : RemoteViews.InteractionHandler { - /** - * Responsible for starting the pending intent for launching activities. - */ + /** Responsible for starting the pending intent for launching activities. */ fun interface IntentStarter { fun startPendingIntent( intent: PendingIntent, @@ -57,7 +57,10 @@ class InteractionHandlerDelegate( // activities. val hostView = getNearestParent(view) val animationController = - hostView?.let(ActivityTransitionAnimator.Controller::fromView) + hostView?.let(ActivityTransitionAnimator.Controller::fromView)?.let { + communalSceneInteractor.setIsLaunchingWidget(true) + CommunalTransitionAnimatorController(it, communalSceneInteractor) + } val (fillInIntent, activityOptions) = launchOptions intentStarter.startPendingIntent( pendingIntent, @@ -66,7 +69,6 @@ class InteractionHandlerDelegate( animationController ) } - else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt new file mode 100644 index 000000000000..4efaf878f33f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt @@ -0,0 +1,50 @@ +/* + * 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.communal.widgets + +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.DelegateTransitionAnimatorController +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes + +/** + * An [ActivityTransitionAnimator.Controller] that takes care of updating the state of the Communal + * Hub at the right time. + */ +class CommunalTransitionAnimatorController( + delegate: ActivityTransitionAnimator.Controller, + private val communalSceneInteractor: CommunalSceneInteractor, +) : DelegateTransitionAnimatorController(delegate) { + override fun onIntentStarted(willAnimate: Boolean) { + if (!willAnimate) { + // Other callbacks won't happen, so reset the state here. + communalSceneInteractor.setIsLaunchingWidget(false) + } + delegate.onIntentStarted(willAnimate) + } + + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { + communalSceneInteractor.setIsLaunchingWidget(false) + delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + communalSceneInteractor.snapToScene(CommunalScenes.Blank) + communalSceneInteractor.setIsLaunchingWidget(false) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt index 72f9180c51d2..519903e55c50 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.view.View import android.widget.RemoteViews import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.util.InteractionHandlerDelegate import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter @@ -32,10 +33,12 @@ class WidgetInteractionHandler @Inject constructor( private val activityStarter: ActivityStarter, + private val communalSceneInteractor: CommunalSceneInteractor ) : RemoteViews.InteractionHandler { private val delegate = InteractionHandlerDelegate( + communalSceneInteractor, findViewToAnimate = { view -> view is CommunalAppWidgetHostView }, intentStarter = this::startIntent, ) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index fa52dad17966..9460eaf8abca 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -57,16 +57,13 @@ import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter -import java.util.Arrays import java.util.concurrent.Executor -import java.util.stream.Collectors import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -170,7 +167,6 @@ constructor( ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null private var detectCancellationSignal: CancellationSignal? = null - private var faceAcquiredInfoIgnoreList: Set<Int> private var retryCount = 0 private var pendingAuthenticateRequest = MutableStateFlow<AuthenticationRequest?>(null) @@ -240,14 +236,6 @@ constructor( faceManager?.addLockoutResetCallback(faceLockoutResetCallback) faceAuthLogger.addLockoutResetCallbackDone() } - faceAcquiredInfoIgnoreList = - Arrays.stream( - context.resources.getIntArray( - R.array.config_face_acquire_device_entry_ignorelist - ) - ) - .boxed() - .collect(Collectors.toSet()) dumpManager.registerCriticalDumpable("DeviceEntryFaceAuthRepositoryImpl", this) canRunFaceAuth = @@ -485,10 +473,8 @@ constructor( } override fun onAuthenticationHelp(code: Int, helpStr: CharSequence?) { - if (faceAcquiredInfoIgnoreList.contains(code)) { - return - } - _authenticationStatus.value = HelpFaceAuthenticationStatus(code, helpStr.toString()) + _authenticationStatus.value = + HelpFaceAuthenticationStatus(code, helpStr?.toString()) } override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) { @@ -731,7 +717,6 @@ constructor( pw.println(" _pendingAuthenticateRequest: ${pendingAuthenticateRequest.value}") pw.println(" authCancellationSignal: $authCancellationSignal") pw.println(" detectCancellationSignal: $detectCancellationSignal") - pw.println(" faceAcquiredInfoIgnoreList: $faceAcquiredInfoIgnoreList") pw.println(" _authenticationStatus: ${_authenticationStatus.value}") pw.println(" _detectionStatus: ${_detectionStatus.value}") pw.println(" currentUserId: $currentUserId") diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index 0f189788457c..e2ad7741557f 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -39,12 +39,6 @@ interface DeviceEntryRepository { * the lockscreen. */ val isBypassEnabled: StateFlow<Boolean> - - /** - * Reports, to system server, that the user is "present" now. This is a signal that system - * server uses to know that the device has been entered. - */ - suspend fun reportUserPresent() } /** Encapsulates application state for device entry. */ @@ -84,17 +78,6 @@ constructor( SharingStarted.Eagerly, initialValue = keyguardBypassController.bypassEnabled, ) - - override suspend fun reportUserPresent() { - withContext(backgroundDispatcher) { - val selectedUserId = userRepository.selectedUser.value.userInfo.id - lockPatternUtils.userPresent(selectedUserId) - } - } - - companion object { - private const val TAG = "DeviceEntryRepositoryImpl" - } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt new file mode 100644 index 000000000000..34b1544489bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt @@ -0,0 +1,95 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.res.Resources +import android.hardware.biometrics.BiometricFaceConstants +import com.android.systemui.biometrics.FaceHelpMessageDebouncer +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.res.R +import java.util.Arrays +import java.util.stream.Collectors +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transform + +/** + * Process face authentication statuses. + * - Ignores face help messages based on R.array.config_face_acquire_device_entry_ignorelist. + * - Uses FaceHelpMessageDebouncer to debounce flickery help messages. + */ +@SysUISingleton +class DeviceEntryFaceAuthStatusInteractor +@Inject +constructor( + repository: DeviceEntryFaceAuthRepository, + @Main private val resources: Resources, + @Application private val applicationScope: CoroutineScope, +) { + private val faceHelpMessageDebouncer = FaceHelpMessageDebouncer() + private var faceAcquiredInfoIgnoreList: Set<Int> = + Arrays.stream(resources.getIntArray(R.array.config_face_acquire_device_entry_ignorelist)) + .boxed() + .collect(Collectors.toSet()) + + val authenticationStatus: StateFlow<FaceAuthenticationStatus?> = + repository.authenticationStatus + .transform { authenticationStatus -> + if (authenticationStatus is AcquiredFaceAuthenticationStatus) { + if ( + authenticationStatus.acquiredInfo == + BiometricFaceConstants.FACE_ACQUIRED_START + ) { + faceHelpMessageDebouncer.startNewFaceAuthSession( + authenticationStatus.createdAt + ) + } + } + + if (authenticationStatus is HelpFaceAuthenticationStatus) { + if (!faceAcquiredInfoIgnoreList.contains(authenticationStatus.msgId)) { + faceHelpMessageDebouncer.addMessage(authenticationStatus) + } + + val messageToShow = + faceHelpMessageDebouncer.getMessageToShow( + atTimestamp = authenticationStatus.createdAt, + ) + if (messageToShow != null) { + emit(messageToShow) + } + + return@transform + } + + emit(authenticationStatus) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 425bb965f34d..ea0e59bb6ccc 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -16,33 +16,24 @@ package com.android.systemui.deviceentry.domain.interactor -import androidx.annotation.VisibleForTesting import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository -import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason -import com.android.systemui.flags.SystemPropertiesHelper -import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.util.kotlin.Quad import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -61,12 +52,7 @@ constructor( private val repository: DeviceEntryRepository, private val authenticationInteractor: AuthenticationInteractor, private val sceneInteractor: SceneInteractor, - faceAuthInteractor: DeviceEntryFaceAuthInteractor, - private val fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, - private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, - private val trustInteractor: TrustInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, - private val systemPropertiesHelper: SystemPropertiesHelper, private val alternateBouncerInteractor: AlternateBouncerInteractor, ) { /** @@ -109,11 +95,6 @@ constructor( false } } - .onEach { isDeviceEntered -> - if (isDeviceEntered) { - repository.reportUserPresent() - } - } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -156,70 +137,6 @@ constructor( initialValue = null, ) - private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled - private val fingerprintEnrolledAndEnabled = - biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled - private val trustAgentEnabled = trustInteractor.isEnrolledAndEnabled - - private val faceOrFingerprintOrTrustEnabled: Flow<Triple<Boolean, Boolean, Boolean>> = - combine(faceEnrolledAndEnabled, fingerprintEnrolledAndEnabled, trustAgentEnabled, ::Triple) - - /** - * Reason why device entry is restricted to certain authentication methods for the current user. - * - * Emits null when there are no device entry restrictions active. - */ - val deviceEntryRestrictionReason: Flow<DeviceEntryRestrictionReason?> = - faceOrFingerprintOrTrustEnabled.flatMapLatest { - (faceEnabled, fingerprintEnabled, trustEnabled) -> - if (faceEnabled || fingerprintEnabled || trustEnabled) { - combine( - biometricSettingsInteractor.authenticationFlags, - faceAuthInteractor.isLockedOut, - fingerprintAuthInteractor.isLockedOut, - trustInteractor.isTrustAgentCurrentlyAllowed, - ::Quad - ) - .map { (authFlags, isFaceLockedOut, isFingerprintLockedOut, trustManaged) -> - when { - authFlags.isPrimaryAuthRequiredAfterReboot && - wasRebootedForMainlineUpdate -> - DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate - authFlags.isPrimaryAuthRequiredAfterReboot -> - DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot - authFlags.isPrimaryAuthRequiredAfterDpmLockdown -> - DeviceEntryRestrictionReason.PolicyLockdown - authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown - authFlags.isPrimaryAuthRequiredForUnattendedUpdate -> - DeviceEntryRestrictionReason.UnattendedUpdate - authFlags.isPrimaryAuthRequiredAfterTimeout -> - DeviceEntryRestrictionReason.SecurityTimeout - authFlags.isPrimaryAuthRequiredAfterLockout -> - DeviceEntryRestrictionReason.BouncerLockedOut - isFingerprintLockedOut -> - DeviceEntryRestrictionReason.StrongBiometricsLockedOut - isFaceLockedOut && faceAuthInteractor.isFaceAuthStrong() -> - DeviceEntryRestrictionReason.StrongBiometricsLockedOut - isFaceLockedOut -> DeviceEntryRestrictionReason.NonStrongFaceLockedOut - authFlags.isSomeAuthRequiredAfterAdaptiveAuthRequest -> - DeviceEntryRestrictionReason.AdaptiveAuthRequest - (trustEnabled && !trustManaged) && - (authFlags.someAuthRequiredAfterTrustAgentExpired || - authFlags.someAuthRequiredAfterUserRequest) -> - DeviceEntryRestrictionReason.TrustAgentDisabled - authFlags.strongerAuthRequiredAfterNonStrongBiometricsTimeout -> - DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout - else -> null - } - } - } else { - flowOf(null) - } - } - - /** Whether the device is in lockdown mode, where bouncer input is required to unlock. */ - val isInLockdown: Flow<Boolean> = deviceEntryRestrictionReason.map { it.isInLockdown() } - /** * Attempt to enter the device and dismiss the lockscreen. If authentication is required to * unlock the device it will transition to bouncer. @@ -268,27 +185,6 @@ constructor( return repository.isLockscreenEnabled() } - fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean { - return when (this) { - DeviceEntryRestrictionReason.UserLockdown -> true - DeviceEntryRestrictionReason.PolicyLockdown -> true - - // Add individual enum value instead of using "else" so new reasons are guaranteed - // to be added here at compile-time. - null -> false - DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> false - DeviceEntryRestrictionReason.BouncerLockedOut -> false - DeviceEntryRestrictionReason.AdaptiveAuthRequest -> false - DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> false - DeviceEntryRestrictionReason.TrustAgentDisabled -> false - DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> false - DeviceEntryRestrictionReason.SecurityTimeout -> false - DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> false - DeviceEntryRestrictionReason.UnattendedUpdate -> false - DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> false - } - } - /** * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically * dismissed once the authentication challenge is completed. For example, completing a biometric @@ -296,12 +192,4 @@ constructor( * lockscreen. */ val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled - - private val wasRebootedForMainlineUpdate - get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE - - companion object { - @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" - @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" - } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index 51416905b0c0..e17e530a03d9 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -16,20 +16,26 @@ package com.android.systemui.deviceentry.domain.interactor +import androidx.annotation.VisibleForTesting import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus +import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -49,6 +55,8 @@ constructor( faceAuthInteractor: DeviceEntryFaceAuthInteractor, fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val powerInteractor: PowerInteractor, + private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, + private val systemPropertiesHelper: SystemPropertiesHelper, ) { private val deviceUnlockSource = @@ -69,6 +77,75 @@ constructor( .map { DeviceUnlockSource.BouncerInput } ) + private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled + private val fingerprintEnrolledAndEnabled = + biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled + private val trustAgentEnabled = trustInteractor.isEnrolledAndEnabled + + private val faceOrFingerprintOrTrustEnabled: Flow<Triple<Boolean, Boolean, Boolean>> = + combine(faceEnrolledAndEnabled, fingerprintEnrolledAndEnabled, trustAgentEnabled, ::Triple) + + /** + * Reason why device entry is restricted to certain authentication methods for the current user. + * + * Emits null when there are no device entry restrictions active. + */ + val deviceEntryRestrictionReason: Flow<DeviceEntryRestrictionReason?> = + faceOrFingerprintOrTrustEnabled.flatMapLatest { + (faceEnabled, fingerprintEnabled, trustEnabled) -> + if (faceEnabled || fingerprintEnabled || trustEnabled) { + combine( + biometricSettingsInteractor.authenticationFlags, + faceAuthInteractor.isLockedOut, + fingerprintAuthInteractor.isLockedOut, + trustInteractor.isTrustAgentCurrentlyAllowed, + ) { authFlags, isFaceLockedOut, isFingerprintLockedOut, trustManaged -> + when { + authFlags.isPrimaryAuthRequiredAfterReboot && + wasRebootedForMainlineUpdate() -> + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate + authFlags.isPrimaryAuthRequiredAfterReboot -> + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot + authFlags.isPrimaryAuthRequiredAfterDpmLockdown -> + DeviceEntryRestrictionReason.PolicyLockdown + authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown + authFlags.isPrimaryAuthRequiredForUnattendedUpdate -> + DeviceEntryRestrictionReason.UnattendedUpdate + authFlags.isPrimaryAuthRequiredAfterTimeout -> + DeviceEntryRestrictionReason.SecurityTimeout + authFlags.isPrimaryAuthRequiredAfterLockout -> + DeviceEntryRestrictionReason.BouncerLockedOut + isFingerprintLockedOut -> + DeviceEntryRestrictionReason.StrongBiometricsLockedOut + isFaceLockedOut && faceAuthInteractor.isFaceAuthStrong() -> + DeviceEntryRestrictionReason.StrongBiometricsLockedOut + isFaceLockedOut -> DeviceEntryRestrictionReason.NonStrongFaceLockedOut + authFlags.isSomeAuthRequiredAfterAdaptiveAuthRequest -> + DeviceEntryRestrictionReason.AdaptiveAuthRequest + (trustEnabled && !trustManaged) && + (authFlags.someAuthRequiredAfterTrustAgentExpired || + authFlags.someAuthRequiredAfterUserRequest) -> + DeviceEntryRestrictionReason.TrustAgentDisabled + authFlags.strongerAuthRequiredAfterNonStrongBiometricsTimeout -> + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout + else -> null + } + } + } else { + biometricSettingsInteractor.authenticationFlags.map { authFlags -> + when { + authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown + authFlags.isPrimaryAuthRequiredAfterDpmLockdown -> + DeviceEntryRestrictionReason.PolicyLockdown + else -> null + } + } + } + } + + /** Whether the device is in lockdown mode, where bouncer input is required to unlock. */ + val isInLockdown: Flow<Boolean> = deviceEntryRestrictionReason.map { it.isInLockdown() } + /** * Whether the device is unlocked or not, along with the information about the authentication * method that was used to unlock the device. @@ -90,13 +167,18 @@ constructor( // Device is locked if SIM is locked. flowOf(DeviceUnlockStatus(false, null)) } else { - powerInteractor.isAsleep.flatMapLatest { isAsleep -> - if (isAsleep) { - flowOf(DeviceUnlockStatus(false, null)) - } else { - deviceUnlockSource.map { DeviceUnlockStatus(true, it) } + combine( + powerInteractor.isAsleep, + isInLockdown, + ::Pair, + ) + .flatMapLatestConflated { (isAsleep, isInLockdown) -> + if (isAsleep || isInLockdown) { + flowOf(DeviceUnlockStatus(false, null)) + } else { + deviceUnlockSource.map { DeviceUnlockStatus(true, it) } + } } - } } } .stateIn( @@ -104,4 +186,34 @@ constructor( started = SharingStarted.Eagerly, initialValue = DeviceUnlockStatus(false, null), ) + + private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean { + return when (this) { + DeviceEntryRestrictionReason.UserLockdown -> true + DeviceEntryRestrictionReason.PolicyLockdown -> true + + // Add individual enum value instead of using "else" so new reasons are guaranteed + // to be added here at compile-time. + null -> false + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> false + DeviceEntryRestrictionReason.BouncerLockedOut -> false + DeviceEntryRestrictionReason.AdaptiveAuthRequest -> false + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> false + DeviceEntryRestrictionReason.TrustAgentDisabled -> false + DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> false + DeviceEntryRestrictionReason.SecurityTimeout -> false + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> false + DeviceEntryRestrictionReason.UnattendedUpdate -> false + DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> false + } + } + + private fun wasRebootedForMainlineUpdate(): Boolean { + return systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE + } + + companion object { + @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" + @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index d12ea4573fbe..c536d6b4f6f8 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -90,6 +90,7 @@ constructor( private val powerInteractor: PowerInteractor, private val biometricSettingsRepository: BiometricSettingsRepository, private val trustManager: TrustManager, + deviceEntryFaceAuthStatusInteractor: DeviceEntryFaceAuthStatusInteractor, ) : DeviceEntryFaceAuthInteractor { private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf() @@ -276,9 +277,13 @@ constructor( } private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null) + /** Provide the status of face authentication */ override val authenticationStatus = - merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus) + merge( + faceAuthenticationStatusOverride.filterNotNull(), + deviceEntryFaceAuthStatusInteractor.authenticationStatus.filterNotNull(), + ) /** Provide the status of face detection */ override val detectionStatus = repository.detectionStatus diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 7ae840938fe7..e07b5c228585 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -48,6 +48,7 @@ import com.android.internal.R; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; +import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.plugins.SensorManagerPlugin; import com.android.systemui.statusbar.phone.DozeParameters; @@ -426,7 +427,11 @@ public class DozeSensors { } if (!anyListening) { - mSecureSettings.unregisterContentObserverSync(mSettingsObserver); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.unregisterContentObserverAsync(mSettingsObserver); + } else { + mSecureSettings.unregisterContentObserverSync(mSettingsObserver); + } } else if (!mSettingRegistered) { for (TriggerSensor s : mTriggerSensors) { s.registerSettingsObserver(mSettingsObserver); @@ -750,8 +755,13 @@ public class DozeSensors { public void registerSettingsObserver(ContentObserver settingsObserver) { if (mConfigured && !TextUtils.isEmpty(mSetting)) { - mSecureSettings.registerContentObserverForUserSync( - mSetting, mSettingsObserver, UserHandle.USER_ALL); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.registerContentObserverForUserAsync( + mSetting, mSettingsObserver, UserHandle.USER_ALL); + } else { + mSecureSettings.registerContentObserverForUserSync( + mSetting, mSettingsObserver, UserHandle.USER_ALL); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index e2bcb6bc2457..53b9261991e0 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -17,8 +17,12 @@ package com.android.systemui.education.dagger import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.education.data.repository.ContextualEducationRepository +import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl +import dagger.Binds import dagger.Module import dagger.Provides +import java.time.Clock import javax.inject.Qualifier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -26,8 +30,15 @@ import kotlinx.coroutines.SupervisorJob @Module interface ContextualEducationModule { + @Binds + fun bindContextualEducationRepository( + impl: ContextualEducationRepositoryImpl + ): ContextualEducationRepository + @Qualifier annotation class EduDataStoreScope + @Qualifier annotation class EduClock + companion object { @EduDataStoreScope @Provides @@ -36,5 +47,11 @@ interface ContextualEducationModule { ): CoroutineScope { return CoroutineScope(bgDispatcher + SupervisorJob()) } + + @EduClock + @Provides + fun provideEduClock(): Clock { + return Clock.systemUTC() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt index af35e8c3662b..9f6cb4d027e6 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt @@ -16,11 +16,14 @@ package com.android.systemui.education.data.model +import java.time.Instant + /** * Model to store education data related to each gesture (e.g. Back, Home, All Apps, Overview). Each * gesture stores its own model separately. */ data class GestureEduModel( - val signalCount: Int, - val educationShownCount: Int, + val signalCount: Int = 0, + val educationShownCount: Int = 0, + val lastShortcutTriggeredTime: Instant? = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt index c9dd833dac75..248b7a526256 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt @@ -17,26 +17,50 @@ package com.android.systemui.education.data.repository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.education.dagger.ContextualEducationModule.EduClock +import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.shared.education.GestureType +import java.time.Clock import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates the functions of ContextualEducationRepository. */ +interface ContextualEducationRepository { + fun setUser(userId: Int) + + fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> + + suspend fun incrementSignalCount(gestureType: GestureType) + + suspend fun updateShortcutTriggerTime(gestureType: GestureType) +} /** * Provide methods to read and update on field level and allow setting datastore when user is * changed */ @SysUISingleton -class ContextualEducationRepository +class ContextualEducationRepositoryImpl @Inject -constructor(private val userEduRepository: UserContextualEducationRepository) { +constructor( + @EduClock private val clock: Clock, + private val userEduRepository: UserContextualEducationRepository +) : ContextualEducationRepository { /** To change data store when user is changed */ - fun setUser(userId: Int) = userEduRepository.setUser(userId) + override fun setUser(userId: Int) = userEduRepository.setUser(userId) - fun readGestureEduModelFlow(gestureType: GestureType) = + override fun readGestureEduModelFlow(gestureType: GestureType) = userEduRepository.readGestureEduModelFlow(gestureType) - suspend fun incrementSignalCount(gestureType: GestureType) { + override suspend fun incrementSignalCount(gestureType: GestureType) { userEduRepository.updateGestureEduModel(gestureType) { it.copy(signalCount = it.signalCount + 1) } } + + override suspend fun updateShortcutTriggerTime(gestureType: GestureType) { + userEduRepository.updateGestureEduModel(gestureType) { + it.copy(lastShortcutTriggeredTime = clock.instant()) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 229511a20caf..b7fc7733f3e6 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -18,16 +18,19 @@ package com.android.systemui.education.data.repository import android.content.Context import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.shared.education.GestureType +import java.time.Instant import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineScope @@ -55,6 +58,7 @@ constructor( companion object { const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" + const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" const val DATASTORE_DIR = "education/USER%s_ContextualEducation" } @@ -91,6 +95,10 @@ constructor( return GestureEduModel( signalCount = preferences[getSignalCountKey(gestureType)] ?: 0, educationShownCount = preferences[getEducationShownCountKey(gestureType)] ?: 0, + lastShortcutTriggeredTime = + preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let { + Instant.ofEpochMilli(it) + }, ) } @@ -103,6 +111,11 @@ constructor( val updatedModel = transform(currentModel) preferences[getSignalCountKey(gestureType)] = updatedModel.signalCount preferences[getEducationShownCountKey(gestureType)] = updatedModel.educationShownCount + updateTimeByInstant( + preferences, + updatedModel.lastShortcutTriggeredTime, + getLastShortcutTriggeredTimeKey(gestureType) + ) } } @@ -111,4 +124,19 @@ constructor( private fun getEducationShownCountKey(gestureType: GestureType): Preferences.Key<Int> = intPreferencesKey(gestureType.name + NUMBER_OF_EDU_SHOWN_SUFFIX) + + private fun getLastShortcutTriggeredTimeKey(gestureType: GestureType): Preferences.Key<Long> = + longPreferencesKey(gestureType.name + LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX) + + private fun updateTimeByInstant( + preferences: MutablePreferences, + instant: Instant?, + key: Preferences.Key<Long> + ) { + if (instant != null) { + preferences[key] = instant.toEpochMilli() + } else { + preferences.remove(key) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 1e4fb4f15062..493afde96aff 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -64,7 +64,6 @@ import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.service.dreams.IDreamManager; import android.sysprop.TelephonyProperties; import android.telecom.TelecomManager; import android.telephony.ServiceState; @@ -197,7 +196,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final Context mContext; private final GlobalActionsManager mWindowManagerFuncs; private final AudioManager mAudioManager; - private final IDreamManager mDreamManager; private final DevicePolicyManager mDevicePolicyManager; private final LockPatternUtils mLockPatternUtils; private final SelectedUserInteractor mSelectedUserInteractor; @@ -345,7 +343,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Context context, GlobalActionsManager windowManagerFuncs, AudioManager audioManager, - IDreamManager iDreamManager, DevicePolicyManager devicePolicyManager, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, @@ -382,7 +379,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; - mDreamManager = iDreamManager; mDevicePolicyManager = devicePolicyManager; mLockPatternUtils = lockPatternUtils; mTelephonyListenerManager = telephonyListenerManager; @@ -510,20 +506,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mHandler.sendEmptyMessage(MESSAGE_DISMISS); } - protected void awakenIfNecessary() { - if (mDreamManager != null) { - try { - if (mDreamManager.isDreaming()) { - mDreamManager.awaken(); - } - } catch (RemoteException e) { - // we tried - } - } - } - protected void handleShow(@Nullable Expandable expandable) { - awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index bbc3d7699dee..89c717892d0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -168,8 +168,7 @@ sealed class TransitionInteractor( */ @Deprecated("Will be merged into maybeStartTransitionToOccludedOrInsecureCamera") suspend fun maybeHandleInsecurePowerGesture(): Boolean { - // TODO(b/336576536): Check if adaptation for scene framework is needed - if (SceneContainerFlag.isEnabled) return true + if (SceneContainerFlag.isEnabled) return false if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) { if (keyguardInteractor.isKeyguardDismissible.value) { startTransitionTo( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt index 853f1769994e..1fd609dba637 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager import android.widget.ProgressBar import androidx.core.view.isGone +import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import javax.inject.Inject @@ -37,7 +38,7 @@ class SideFpsProgressBar @Inject constructor( private val layoutInflater: LayoutInflater, - private val windowManager: WindowManager, + private val windowManager: ViewCaptureAwareWindowManager, ) { private var overlayView: View? = null @@ -90,7 +91,7 @@ constructor( ) { if (overlayView == null) { overlayView = layoutInflater.inflate(R.layout.sidefps_progress_bar, null, false) - windowManager.addView(overlayView, overlayViewParams) + windowManager.addView(requireNotNull(overlayView), overlayViewParams) progressBar?.pivotX = 0.0f progressBar?.pivotY = 0.0f } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt index 754ed6cc3327..1ee0368f15b1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -45,6 +46,13 @@ constructor( edge = Edge.create(from = DREAMING, to = AOD), ) + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = 300.milliseconds, + onStep = { it }, + ) + val deviceEntryBackgroundViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 7c468071d1f8..350ceb47848f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -91,8 +91,9 @@ constructor( private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, - private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, + private val dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel, private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel, + private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, @@ -243,6 +244,7 @@ constructor( dozingToGoneTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), + dreamingToAodTransitionViewModel.lockscreenAlpha, dreamingToGoneTransitionViewModel.lockscreenAlpha, dreamingToLockscreenTransitionViewModel.lockscreenAlpha, glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 3b337fc3f7c8..4bfefda63466 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources +import com.android.compose.animation.scene.SceneKey import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton @@ -26,14 +27,17 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -57,19 +61,6 @@ constructor( val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide - val areNotificationsVisible: StateFlow<Boolean> = - combine( - clockSize, - shadeInteractor.isShadeLayoutWide, - ) { clockSize, isShadeLayoutWide -> - clockSize == ClockSize.SMALL || isShadeLayoutWide - } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - /** Amount of horizontal translation that should be applied to elements in the scene. */ val unfoldTranslations: StateFlow<UnfoldTranslations> = combine( @@ -97,6 +88,25 @@ constructor( initialValue = true, ) + /** + * Returns a flow that indicates whether lockscreen notifications should be rendered in the + * given [sceneKey]. + */ + fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> { + // `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's + // open we avoid rendering the lockscreen notifications stack. + if (sceneKey == Scenes.NotificationsShade) { + return flowOf(false) + } + + return combine( + clockSize, + shadeInteractor.isShadeLayoutWide, + ) { clockSize, isShadeLayoutWide -> + clockSize == ClockSize.SMALL || isShadeLayoutWide + } + } + fun getSmartSpacePaddingTop(resources: Resources): Int { return if (clockSize.value == ClockSize.LARGE) { resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt new file mode 100644 index 000000000000..a80bc0975197 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.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.mediaprojection + +import javax.inject.Qualifier + +/** Logs for media projection related events. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class MediaProjectionLog diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt index 34894599aaf9..7fd77a9bdb00 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt @@ -16,12 +16,25 @@ package com.android.systemui.mediaprojection +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository import dagger.Binds import dagger.Module +import dagger.Provides @Module interface MediaProjectionModule { @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository + + companion object { + @Provides + @SysUISingleton + @MediaProjectionLog + fun provideMediaProjectionLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("MediaProjection", 50) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt index c90f1970671d..5704e8048b7d 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt @@ -21,7 +21,6 @@ import android.hardware.display.DisplayManager import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Handler -import android.util.Log import android.view.ContentRecordingSession import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging @@ -29,6 +28,9 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.mediaprojection.MediaProjectionLog import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository @@ -56,22 +58,27 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val tasksRepository: TasksRepository, private val mediaProjectionServiceHelper: MediaProjectionServiceHelper, + @MediaProjectionLog private val logger: LogBuffer, ) : MediaProjectionRepository { override suspend fun switchProjectedTask(task: RunningTaskInfo) { withContext(backgroundDispatcher) { if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) { - Log.d(TAG, "Successfully switched projected task") + logger.log(TAG, LogLevel.DEBUG, {}, { "Successfully switched projected task" }) } else { - Log.d(TAG, "Failed to switch projected task") + logger.log(TAG, LogLevel.WARNING, {}, { "Failed to switch projected task" }) } } } override suspend fun stopProjecting() { withContext(backgroundDispatcher) { - // TODO(b/332662551): Convert Logcat to LogBuffer. - Log.d(TAG, "Requesting MediaProjectionManager#stopActiveProjection") + logger.log( + TAG, + LogLevel.DEBUG, + {}, + { "Requesting MediaProjectionManager#stopActiveProjection" }, + ) mediaProjectionManager.stopActiveProjection() } } @@ -81,12 +88,22 @@ constructor( val callback = object : MediaProjectionManager.Callback() { override fun onStart(info: MediaProjectionInfo?) { - Log.d(TAG, "MediaProjectionManager.Callback#onStart") + logger.log( + TAG, + LogLevel.DEBUG, + {}, + { "MediaProjectionManager.Callback#onStart" }, + ) trySendWithFailureLogging(CallbackEvent.OnStart, TAG) } override fun onStop(info: MediaProjectionInfo?) { - Log.d(TAG, "MediaProjectionManager.Callback#onStop") + logger.log( + TAG, + LogLevel.DEBUG, + {}, + { "MediaProjectionManager.Callback#onStop" }, + ) trySendWithFailureLogging(CallbackEvent.OnStop, TAG) } @@ -94,7 +111,12 @@ constructor( info: MediaProjectionInfo, session: ContentRecordingSession? ) { - Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session") + logger.log( + TAG, + LogLevel.DEBUG, + { str1 = session.toString() }, + { "MediaProjectionManager.Callback#onSessionStarted: $str1" }, + ) trySendWithFailureLogging( CallbackEvent.OnRecordingSessionSet(info, session), TAG, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 18358a79cbca..d8c13b6e8f6d 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -33,6 +33,7 @@ import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation +import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker @@ -83,7 +84,7 @@ private const val DEBUG = false class BackPanelController internal constructor( context: Context, - private val windowManager: WindowManager, + private val windowManager: ViewCaptureAwareWindowManager, private val viewConfiguration: ViewConfiguration, private val mainHandler: Handler, private val systemClock: SystemClock, @@ -102,7 +103,7 @@ internal constructor( class Factory @Inject constructor( - private val windowManager: WindowManager, + private val windowManager: ViewCaptureAwareWindowManager, private val viewConfiguration: ViewConfiguration, @BackPanelUiThread private val uiThreadContext: UiThreadContext, private val systemClock: SystemClock, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index c9be9938ef74..947336d41590 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -91,6 +91,7 @@ import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.util.concurrency.BackPanelUiThread; import com.android.systemui.util.concurrency.UiThreadContext; @@ -297,6 +298,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private Date mTmpLogDate = new Date(); private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver; + private final NotificationShadeWindowController mNotificationShadeWindowController; private final NavigationEdgeBackPlugin.BackCallback mBackCallback = new NavigationEdgeBackPlugin.BackCallback() { @@ -423,7 +425,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack Optional<DesktopMode> desktopModeOptional, FalsingManager falsingManager, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, - Provider<LightBarController> lightBarControllerProvider) { + Provider<LightBarController> lightBarControllerProvider, + NotificationShadeWindowController notificationShadeWindowController) { mContext = context; mDisplayId = context.getDisplayId(); mUiThreadContext = uiThreadContext; @@ -479,6 +482,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack this::onNavigationSettingsChanged); updateCurrentUserResources(); + mNotificationShadeWindowController = notificationShadeWindowController; } public void setStateChangeCallback(Runnable callback) { @@ -1297,6 +1301,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mBackAnimation.setPilferPointerCallback(() -> { pilferPointers(); }); + mBackAnimation.setTopUiRequestCallback( + (requestTopUi, tag) -> mUiThreadContext.getExecutor().execute(() -> + mNotificationShadeWindowController.setRequestTopUi(requestTopUi, tag))); updateBackAnimationThresholds(); if (mLightBarControllerProvider.get() != null) { mBackAnimation.setStatusBarCustomizer((appearance) -> { @@ -1333,6 +1340,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Provider<BackGestureTfClassifierProvider> mBackGestureTfClassifierProviderProvider; private final Provider<LightBarController> mLightBarControllerProvider; + private final NotificationShadeWindowController mNotificationShadeWindowController; @Inject public Factory(OverviewProxyService overviewProxyService, @@ -1353,7 +1361,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack FalsingManager falsingManager, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, - Provider<LightBarController> lightBarControllerProvider) { + Provider<LightBarController> lightBarControllerProvider, + NotificationShadeWindowController notificationShadeWindowController) { mOverviewProxyService = overviewProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; @@ -1372,6 +1381,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mFalsingManager = falsingManager; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mLightBarControllerProvider = lightBarControllerProvider; + mNotificationShadeWindowController = notificationShadeWindowController; } /** Construct a {@link EdgeBackGestureHandler}. */ @@ -1396,7 +1406,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mDesktopModeOptional, mFalsingManager, mBackGestureTfClassifierProviderProvider, - mLightBarControllerProvider)); + mLightBarControllerProvider, + mNotificationShadeWindowController)); } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index e832abb420f0..afdfa5932162 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -40,6 +40,8 @@ import static com.android.systemui.navigationbar.NavBarHelper.transitionMode; import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; import static com.android.systemui.shared.rotation.RotationButtonController.DEBUG_ROTATION; +import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE; +import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY; @@ -48,8 +50,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_I import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; -import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE; -import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode; import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG_WINDOW_STATE; import static com.android.systemui.statusbar.phone.CentralSurfaces.dumpBarTransitions; import static com.android.systemui.util.Utils.isGesturalModeOnDefaultDisplay; @@ -97,6 +97,7 @@ import android.view.WindowInsetsController.Behavior; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import androidx.annotation.Nullable; @@ -117,17 +118,17 @@ import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; -import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger; import com.android.systemui.navigationbar.NavBarHelper; import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener; +import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; +import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle; import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.views.buttons.DeadZone; import com.android.systemui.navigationbar.views.buttons.KeyButtonView; +import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger; import com.android.systemui.navigationbar.views.buttons.NavbarOrientationTrackingLogger; -import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; -import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.recents.Recents; @@ -1348,6 +1349,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements ButtonDispatcher imeSwitcherButton = mView.getImeSwitchButton(); imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick); + if (Flags.imeSwitcherRevamp()) { + imeSwitcherButton.setOnLongClickListener(this::onImeSwitcherLongClick); + } updateScreenPinningGestures(); } @@ -1501,12 +1505,29 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mCommandQueue.toggleRecentApps(); } - private void onImeSwitcherClick(View v) { + @VisibleForTesting + void onImeSwitcherClick(View v) { + mNavBarButtonClickLogger.logImeSwitcherClick(); + if (Flags.imeSwitcherRevamp()) { + mInputMethodManager.onImeSwitchButtonClickFromSystem(mDisplayId); + } else { + mInputMethodManager.showInputMethodPickerFromSystem( + true /* showAuxiliarySubtypes */, mDisplayId); + } + mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); + } + + @VisibleForTesting + boolean onImeSwitcherLongClick(View v) { + if (!Flags.imeSwitcherRevamp()) { + return false; + } mNavBarButtonClickLogger.logImeSwitcherClick(); mInputMethodManager.showInputMethodPickerFromSystem( true /* showAuxiliarySubtypes */, mDisplayId); - mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); - }; + mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); + return true; + } private boolean onLongPressBackHome(View v) { return onLongPressNavigationButtons(v, R.id.back, R.id.home); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java index 0f360973019f..1dbd500c15f1 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java @@ -19,7 +19,6 @@ package com.android.systemui.navigationbar.views; import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; -import static com.android.systemui.Flags.enableViewCaptureTracing; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SEARCH_DISABLED; @@ -38,7 +37,6 @@ import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; -import android.media.permission.SafeCloseable; import android.os.Bundle; import android.os.RemoteException; import android.util.AttributeSet; @@ -61,19 +59,18 @@ import android.widget.FrameLayout; import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; -import com.android.app.viewcapture.ViewCaptureFactory; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.ScreenPinningNotify; +import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.views.buttons.ContextualButton; import com.android.systemui.navigationbar.views.buttons.ContextualButtonGroup; import com.android.systemui.navigationbar.views.buttons.DeadZone; import com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable; import com.android.systemui.navigationbar.views.buttons.NearestTouchFrame; -import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.recents.Recents; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; @@ -181,7 +178,6 @@ public class NavigationBarView extends FrameLayout { private boolean mOverviewProxyEnabled; private boolean mShowSwipeUpUi; private UpdateActiveTouchRegionsCallback mUpdateActiveTouchRegionsCallback; - private SafeCloseable mViewCaptureCloseable; private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; @@ -1082,10 +1078,6 @@ public class NavigationBarView extends FrameLayout { } updateNavButtonIcons(); - if (enableViewCaptureTracing()) { - mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext()) - .startCapture(getRootView(), ".NavigationBarView"); - } } @Override @@ -1098,9 +1090,6 @@ public class NavigationBarView extends FrameLayout { mFloatingRotationButton.hide(); mRotationButtonController.unregisterListeners(); } - if (mViewCaptureCloseable != null) { - mViewCaptureCloseable.close(); - } } void dump(PrintWriter pw) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java index 1e85d6c9237b..133d14ddb9f4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java @@ -112,6 +112,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @UiEvent(doc = "The overview button was long-pressed in the navigation bar.") NAVBAR_OVERVIEW_BUTTON_LONGPRESS(538), + @UiEvent(doc = "The ime switcher button was long-pressed in the navigation bar.") + NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS(1799), + NONE(0); // an event we should not log private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java index d68b22b84f09..4d6cf78610d9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java @@ -29,7 +29,6 @@ import android.provider.Settings; import androidx.annotation.NonNull; -import com.android.server.display.feature.flags.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.UserTracker; @@ -81,10 +80,17 @@ public class ReduceBrightColorsControllerImpl implements mAvailable = true; synchronized (mListeners) { if (mListeners.size() > 0) { - mSecureSettings.unregisterContentObserverSync(mContentObserver); - mSecureSettings.registerContentObserverForUserSync( - Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, - false, mContentObserver, newUser); + if (com.android.systemui.Flags.registerContentObserversAsync()) { + mSecureSettings.unregisterContentObserverAsync(mContentObserver); + mSecureSettings.registerContentObserverForUserAsync( + Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, + false, mContentObserver, newUser); + } else { + mSecureSettings.unregisterContentObserverSync(mContentObserver); + mSecureSettings.registerContentObserverForUserSync( + Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, + false, mContentObserver, newUser); + } } } } @@ -98,9 +104,15 @@ public class ReduceBrightColorsControllerImpl implements if (!mListeners.contains(listener)) { mListeners.add(listener); if (mListeners.size() == 1) { - mSecureSettings.registerContentObserverForUserSync( - Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, - false, mContentObserver, mUserTracker.getUserId()); + if (com.android.systemui.Flags.registerContentObserversAsync()) { + mSecureSettings.registerContentObserverForUserAsync( + Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, + false, mContentObserver, mUserTracker.getUserId()); + } else { + mSecureSettings.registerContentObserverForUserSync( + Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, + false, mContentObserver, mUserTracker.getUserId()); + } } } } @@ -110,7 +122,11 @@ public class ReduceBrightColorsControllerImpl implements public void removeCallback(@androidx.annotation.NonNull Listener listener) { synchronized (mListeners) { if (mListeners.remove(listener) && mListeners.size() == 0) { - mSecureSettings.unregisterContentObserverSync(mContentObserver); + if (com.android.systemui.Flags.registerContentObserversAsync()) { + mSecureSettings.unregisterContentObserverAsync(mContentObserver); + } else { + mSecureSettings.unregisterContentObserverSync(mContentObserver); + } } } } @@ -139,7 +155,8 @@ public class ReduceBrightColorsControllerImpl implements @Override public boolean isInUpgradeMode(Resources resources) { - return Flags.evenDimmer() && resources.getBoolean( + return com.android.server.display.feature.flags.Flags.evenDimmer() + && resources.getBoolean( com.android.internal.R.bool.config_evenDimmerEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt index 295a998e7500..782fb2ad25a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt @@ -48,6 +48,9 @@ class DragAndDropState( val sourceSpec: MutableState<TileSpec?>, private val listState: EditTileListState ) { + val dragInProgress: Boolean + get() = sourceSpec.value != null + /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */ fun currentPosition(): Int { return sourceSpec.value?.let { listState.indexOf(it) } ?: -1 @@ -65,6 +68,12 @@ class DragAndDropState( sourceSpec.value?.let { listState.move(it, targetSpec) } } + fun movedOutOfBounds() { + // Removing the tiles from the current tile grid if it moves out of bounds. This clears + // the spacer and makes it apparent that dropping the tile at that point would remove it. + sourceSpec.value?.let { listState.removeFromCurrent(it) } + } + fun onDrop() { sourceSpec.value = null } @@ -112,6 +121,42 @@ fun Modifier.dragAndDropTile( } /** + * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile + * grid to catch out of bounds drops. + * + * @param dragAndDropState The [DragAndDropState] using the tiles list + * @param onDrop Action to be executed when a [TileSpec] is dropped on the composable + */ +@Composable +fun Modifier.dragAndDropRemoveZone( + dragAndDropState: DragAndDropState, + onDrop: (TileSpec) -> Unit, +): Modifier { + val target = + remember(dragAndDropState) { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + return dragAndDropState.sourceSpec.value?.let { + onDrop(it) + dragAndDropState.onDrop() + true + } ?: false + } + + override fun onEntered(event: DragAndDropEvent) { + dragAndDropState.movedOutOfBounds() + } + } + } + return dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) + }, + target = target, + ) +} + +/** * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list * containers to catch drops outside of tiles. * @@ -128,6 +173,10 @@ fun Modifier.dragAndDropTileList( val target = remember(dragAndDropState) { object : DragAndDropTarget { + override fun onEnded(event: DragAndDropEvent) { + dragAndDropState.onDrop() + } + override fun onDrop(event: DragAndDropEvent): Boolean { return dragAndDropState.sourceSpec.value?.let { onDrop(it, dragAndDropState.currentPosition()) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index 482c498d37d7..34876c4cf7f3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -46,6 +46,18 @@ class EditTileListState(tiles: List<EditTileViewModel>) { tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) } } + /** + * Sets the [TileSpec] as a non-current tile. Use this when a tile is dragged out of the current + * tile grid. + */ + fun removeFromCurrent(tileSpec: TileSpec) { + val fromIndex = indexOf(tileSpec) + if (fromIndex >= 0 && fromIndex < tiles.size) { + // Mark the moving tile as non-current + tiles[fromIndex] = tiles[fromIndex].copy(isCurrent = false) + } + } + fun indexOf(tileSpec: TileSpec): Int { return tiles.indexOfFirst { it.tileSpec == tileSpec } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt index ada774db1c6e..add830e9760d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt @@ -84,7 +84,7 @@ constructor( DefaultEditTileGrid( tiles = tiles, isIconOnly = isIcon, - columns = GridCells.Fixed(columns), + columns = columns, modifier = modifier, onAddTile = onAddTile, onRemoveTile = onRemoveTile, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt index 8ca91d89439b..6c84eddb5e38 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt @@ -305,9 +305,9 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition largeTiles, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, dragAndDropState, + onDoubleTap = onDoubleTap, acceptDrops = { true }, onDrop = onDrop, ) @@ -318,10 +318,10 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition smallTiles, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, showLabels = showLabels, - dragAndDropState = dragAndDropState, acceptDrops = { true }, onDrop = onDrop, ) @@ -332,10 +332,10 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition tilesCustom, ClickAction.ADD, addTileToEnd, - onDoubleTap, isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, showLabels = showLabels, - dragAndDropState = dragAndDropState, acceptDrops = { true }, onDrop = onDrop, ) @@ -372,11 +372,6 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition } } - private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { - val rows = (nTiles + columns - 1) / columns - return ((tileHeight + padding) * rows) - padding - } - /** Fill up the rest of the row if it's not complete. */ private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) { if (nTiles % columns != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt index 770d44124025..3e48245a3d36 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt @@ -100,7 +100,7 @@ constructor( DefaultEditTileGrid( tiles = tiles, isIconOnly = iconTilesViewModel::isIconTile, - columns = GridCells.Fixed(columns), + columns = columns, modifier = modifier, onAddTile = onAddTile, onRemoveTile = onRemoveTile, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index 3fdd7f769cf3..bd7956d7d3e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -23,13 +23,19 @@ import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.text.TextUtils import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy @@ -37,20 +43,31 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -80,6 +97,8 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.TileRow import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState @@ -269,7 +288,7 @@ fun TileLazyGrid( fun DefaultEditTileGrid( tiles: List<EditTileViewModel>, isIconOnly: (TileSpec) -> Boolean, - columns: GridCells, + columns: Int, modifier: Modifier, onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit, @@ -277,84 +296,264 @@ fun DefaultEditTileGrid( ) { val currentListState = rememberEditListState(tiles) val dragAndDropState = rememberDragAndDropState(currentListState) - val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent } - val (otherTilesStock, otherTilesCustom) = - otherTiles - .filter { !dragAndDropState.isMoving(it.tileSpec) } - .partition { it.appName == null } + val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) } - val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position -> onAddTile(tileSpec, position) } - val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ -> - onRemoveTile(tileSpec) - } val onDoubleTap: (TileSpec) -> Unit by rememberUpdatedState { tileSpec -> onResize(tileSpec, !isIconOnly(tileSpec)) } + val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) - TileLazyGrid( - modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd), - columns = columns + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + AnimatedContent( + targetState = dragAndDropState.dragInProgress, + modifier = Modifier.wrapContentSize() + ) { dragIsInProgress -> + EditGridHeader(Modifier.dragAndDropRemoveZone(dragAndDropState, onRemoveTile)) { + if (dragIsInProgress) { + RemoveTileTarget() + } else { + Text(text = "Hold and drag to rearrange tiles.") + } + } + } + + CurrentTilesGrid( + currentTiles, + columns, + tilePadding, + isIconOnly, + onRemoveTile, + onDoubleTap, + dragAndDropState, + onDropAdd, + ) + + // Hide available tiles when dragging + AnimatedVisibility( + visible = !dragAndDropState.dragInProgress, + enter = fadeIn(), + exit = fadeOut() + ) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize() + ) { + EditGridHeader { Text(text = "Hold and drag to add tiles.") } + + AvailableTileGrid( + otherTiles, + columns, + tilePadding, + addTileToEnd, + dragAndDropState, + ) + } + } + + // Drop zone to remove tiles dragged out of the tile grid + Spacer( + modifier = + Modifier.fillMaxWidth() + .weight(1f) + .dragAndDropRemoveZone(dragAndDropState, onRemoveTile) + ) + } + } +} + +@Composable +private fun EditGridHeader( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) ) { - // These Text are just placeholders to see the different sections. Not final UI. - item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) } + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth().height(TileDefaults.EditGridHeaderHeight) + ) { + content() + } + } +} - editTiles( - currentTiles, - ClickAction.REMOVE, - onRemoveTile, - onDoubleTap, - isIconOnly, - indicatePosition = true, - dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropAdd, - ) +@Composable +private fun RemoveTileTarget() { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement(), + modifier = + Modifier.fillMaxHeight() + .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape) + .padding(10.dp) + ) { + Icon(imageVector = Icons.Default.Clear, contentDescription = null) + Text(text = "Remove") + } +} - item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) } +@Composable +private fun CurrentTilesContainer(content: @Composable () -> Unit) { + Box( + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), + shape = RoundedCornerShape(48.dp), + ) + .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) + ) { + content() + } +} + +@Composable +private fun CurrentTilesGrid( + tiles: List<EditTileViewModel>, + columns: Int, + tilePadding: Dp, + isIconOnly: (TileSpec) -> Boolean, + onClick: (TileSpec) -> Unit, + onDoubleTap: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, + onDrop: (TileSpec, Int) -> Unit +) { + val tileHeight = tileHeight() + val currentRows = + remember(tiles) { + calculateRows( + tiles.map { + SizedTile( + it, + if (isIconOnly(it.tileSpec)) { + 1 + } else { + 2 + } + ) + }, + columns + ) + } + val currentGridHeight = gridHeight(currentRows, tileHeight, tilePadding) + // Current tiles + CurrentTilesContainer { + TileLazyGrid( + modifier = + Modifier.height(currentGridHeight) + .dragAndDropTileList(dragAndDropState, { true }, onDrop), + columns = GridCells.Fixed(columns) + ) { + editTiles( + tiles, + ClickAction.REMOVE, + onClick, + isIconOnly, + dragAndDropState, + onDoubleTap = onDoubleTap, + indicatePosition = true, + acceptDrops = { true }, + onDrop = onDrop, + ) + } + } +} +@Composable +private fun AvailableTileGrid( + tiles: List<EditTileViewModel>, + columns: Int, + tilePadding: Dp, + onClick: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, +) { + val (otherTilesStock, otherTilesCustom) = + tiles.filter { !dragAndDropState.isMoving(it.tileSpec) }.partition { it.appName == null } + val availableTileHeight = tileHeight(true) + val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding) + + // Available tiles + TileLazyGrid( + modifier = + Modifier.height(availableGridHeight) + .dragAndDropTileList(dragAndDropState, { false }, { _, _ -> }), + columns = GridCells.Fixed(columns) + ) { editTiles( otherTilesStock, ClickAction.ADD, - addTileToEnd, - onDoubleTap, - isIconOnly, + onClick, + isIconOnly = { true }, dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropRemove, + acceptDrops = { false }, + showLabels = true, ) - - item(span = { GridItemSpan(maxLineSpan) }) { - Text("Custom tiles to add", color = Color.White) - } - editTiles( otherTilesCustom, ClickAction.ADD, - addTileToEnd, - onDoubleTap, - isIconOnly, + onClick, + isIconOnly = { true }, dragAndDropState = dragAndDropState, - acceptDrops = { true }, - onDrop = onDropRemove, + acceptDrops = { false }, + showLabels = true, ) } } +fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { + val rows = (nTiles + columns - 1) / columns + return gridHeight(rows, tileHeight, padding) +} + +fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp { + return ((tileHeight + padding) * rows) - padding +} + +private fun calculateRows(tiles: List<SizedTile<EditTileViewModel>>, columns: Int): Int { + val row = TileRow<EditTileViewModel>(columns) + var count = 0 + + for (tile in tiles) { + if (row.maybeAddTile(tile)) { + if (row.isFull()) { + // Row is full, no need to stretch tiles + count += 1 + row.clear() + } + } else { + count += 1 + row.clear() + row.maybeAddTile(tile) + } + } + if (row.tiles.isNotEmpty()) { + count += 1 + } + return count +} + fun LazyGridScope.editTiles( tiles: List<EditTileViewModel>, clickAction: ClickAction, onClick: (TileSpec) -> Unit, - onDoubleTap: (TileSpec) -> Unit, isIconOnly: (TileSpec) -> Boolean, dragAndDropState: DragAndDropState, acceptDrops: (TileSpec) -> Boolean, - onDrop: (TileSpec, Int) -> Unit, + onDoubleTap: (TileSpec) -> Unit = {}, + onDrop: (TileSpec, Int) -> Unit = { _, _ -> }, showLabels: Boolean = false, indicatePosition: Boolean = false, ) { @@ -534,6 +733,7 @@ private data class TileColors( private object TileDefaults { val TileShape = CircleShape val IconTileWithLabelHeight = 140.dp + val EditGridHeaderHeight = 60.dp @Composable fun activeTileColors(): TileColors = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt index 62bfc72f07f2..ef2c8bfe0e4c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt @@ -151,12 +151,27 @@ constructor( * present, it will be moved to the new position. */ fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) { - // Removing tile if it's already present to insert it at the new index. - if (currentTilesInteractor.currentTilesSpecs.contains(tileSpec)) { - removeTile(tileSpec) + val specs = currentTilesInteractor.currentTilesSpecs.toMutableList() + val currentPosition = specs.indexOf(tileSpec) + + if (currentPosition != -1) { + // No operation needed if the element is already in the list at the right position + if (currentPosition == position) { + return + } + // Removing tile if it's present at a different position to insert it at the new index. + specs.removeAt(currentPosition) + } + + if (position >= 0 && position < specs.size) { + specs.add(position, tileSpec) + } else { + specs.add(tileSpec) } - currentTilesInteractor.addTile(tileSpec, position) + // Setting the new tiles as one operation to avoid UI jank with tiles disappearing and + // reappearing + currentTilesInteractor.setTiles(specs) } /** Immediately removes [tileSpec] from the current tiles. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index df7430a0f729..158eb6eb5e89 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -65,6 +65,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -184,7 +185,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi private GlobalSettings mGlobalSettings; private int mDefaultDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private ConnectivityManager.NetworkCallback mConnectivityManagerNetworkCallback; - private WindowManager mWindowManager; + private ViewCaptureAwareWindowManager mWindowManager; private ToastFactory mToastFactory; private SignalDrawable mSignalDrawable; private SignalDrawable mSecondarySignalDrawable; // For the secondary mobile data sub in DSDS @@ -246,7 +247,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi @Main Handler handler, @Main Executor mainExecutor, BroadcastDispatcher broadcastDispatcher, KeyguardUpdateMonitor keyguardUpdateMonitor, GlobalSettings globalSettings, KeyguardStateController keyguardStateController, - WindowManager windowManager, ToastFactory toastFactory, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, ToastFactory toastFactory, @Background Handler workerHandler, CarrierConfigTracker carrierConfigTracker, LocationController locationController, @@ -278,7 +279,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi mAccessPointController = accessPointController; mWifiIconInjector = new WifiUtils.InternetIconInjector(mContext); mConnectivityManagerNetworkCallback = new DataConnectivityListener(); - mWindowManager = windowManager; + mWindowManager = viewCaptureAwareWindowManager; mToastFactory = toastFactory; mSignalDrawable = new SignalDrawable(mContext); mSecondarySignalDrawable = new SignalDrawable(mContext); diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index b3624ad4ad82..371707d78500 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -72,6 +72,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; @@ -302,10 +303,29 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onImeSwitcherPressed() { // TODO(b/204901476) We're intentionally using the default display for now since // Launcher/Taskbar isn't display aware. + if (Flags.imeSwitcherRevamp()) { + mContext.getSystemService(InputMethodManager.class) + .onImeSwitchButtonClickFromSystem(mDisplayTracker.getDefaultDisplayId()); + } else { + mContext.getSystemService(InputMethodManager.class) + .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */, + mDisplayTracker.getDefaultDisplayId()); + } + mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); + } + + @Override + public void onImeSwitcherLongPress() { + if (!Flags.imeSwitcherRevamp()) { + return; + } + // TODO(b/204901476) We're intentionally using the default display for now since + // Launcher/Taskbar isn't display aware. mContext.getSystemService(InputMethodManager.class) .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */, mDisplayTracker.getDefaultDisplayId()); - mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); + mUiEventLogger.log( + KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 1f4820aa8de6..8711e8878525 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -665,7 +665,7 @@ constructor( keyguardEnabledInteractor.isKeyguardEnabled .sample( combine( - deviceEntryInteractor.isInLockdown, + deviceUnlockedInteractor.isInLockdown, deviceEntryInteractor.isDeviceEntered, ::Pair, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index af1b6e1127ad..46c586163ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -27,7 +27,6 @@ import android.os.Bundle; import android.os.CountDownTimer; import android.os.Process; import android.os.UserHandle; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -60,8 +59,6 @@ import javax.inject.Inject; @SysUISingleton public class RecordingController implements CallbackController<RecordingController.RecordingStateChangeCallback> { - private static final String TAG = "RecordingController"; - private boolean mIsStarting; private boolean mIsRecording; private PendingIntent mStopIntent; @@ -71,6 +68,7 @@ public class RecordingController private final BroadcastDispatcher mBroadcastDispatcher; private final FeatureFlags mFlags; private final UserTracker mUserTracker; + private final RecordingControllerLogger mRecordingControllerLogger; private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate; private final ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory; @@ -102,9 +100,10 @@ public class RecordingController if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) { if (intent.hasExtra(EXTRA_STATE)) { boolean state = intent.getBooleanExtra(EXTRA_STATE, false); + mRecordingControllerLogger.logIntentStateUpdated(state); updateState(state); } else { - Log.e(TAG, "Received update intent with no state"); + mRecordingControllerLogger.logIntentMissingState(); } } } @@ -120,6 +119,7 @@ public class RecordingController FeatureFlags flags, Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver, UserTracker userTracker, + RecordingControllerLogger recordingControllerLogger, MediaProjectionMetricsLogger mediaProjectionMetricsLogger, ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate, ScreenRecordDialogDelegate.Factory screenRecordDialogFactory, @@ -130,6 +130,7 @@ public class RecordingController mDevicePolicyResolver = devicePolicyResolver; mBroadcastDispatcher = broadcastDispatcher; mUserTracker = userTracker; + mRecordingControllerLogger = recordingControllerLogger; mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate; mScreenRecordDialogFactory = screenRecordDialogFactory; @@ -212,9 +213,9 @@ public class RecordingController IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE); mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null, UserHandle.ALL); - Log.d(TAG, "sent start intent"); + mRecordingControllerLogger.logSentStartIntent(); } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was cancelled: " + e.getMessage()); + mRecordingControllerLogger.logPendingIntentCancelled(e); } } }; @@ -227,9 +228,10 @@ public class RecordingController */ public void cancelCountdown() { if (mCountDownTimer != null) { + mRecordingControllerLogger.logCountdownCancelled(); mCountDownTimer.cancel(); } else { - Log.e(TAG, "Timer was null"); + mRecordingControllerLogger.logCountdownCancelErrorNoTimer(); } mIsStarting = false; @@ -258,16 +260,16 @@ public class RecordingController * Stop the recording */ public void stopRecording() { - // TODO(b/332662551): Convert Logcat to LogBuffer. try { if (mStopIntent != null) { + mRecordingControllerLogger.logRecordingStopped(); mStopIntent.send(mInteractiveBroadcastOption); } else { - Log.e(TAG, "Stop intent was null"); + mRecordingControllerLogger.logRecordingStopErrorNoStopIntent(); } updateState(false); } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Error stopping: " + e.getMessage()); + mRecordingControllerLogger.logRecordingStopError(e); } } @@ -276,6 +278,7 @@ public class RecordingController * @param isRecording */ public synchronized void updateState(boolean isRecording) { + mRecordingControllerLogger.logStateUpdated(isRecording); if (!isRecording && mIsRecording) { // Unregister receivers if we have stopped recording mUserTracker.removeCallback(mUserChangedCallback); diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt new file mode 100644 index 000000000000..dd2baef35bd5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt @@ -0,0 +1,28 @@ +/* + * 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.screenrecord + +import javax.inject.Qualifier + +/** + * Logs for screen record events. See [com.android.systemui.screenrecord.RecordingController] and + * [com.android.systemui.screenrecord.RecordingControllerLogger]. + */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class RecordingControllerLog diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt new file mode 100644 index 000000000000..e16c010f32b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt @@ -0,0 +1,77 @@ +/* + * 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.screenrecord + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import javax.inject.Inject + +/** Helper class for logging events to [RecordingControllerLog] from Java. */ +@SysUISingleton +class RecordingControllerLogger +@Inject +constructor( + @RecordingControllerLog private val logger: LogBuffer, +) { + fun logStateUpdated(isRecording: Boolean) = + logger.log( + TAG, + LogLevel.DEBUG, + { bool1 = isRecording }, + { "Updating state. isRecording=$bool1" }, + ) + + fun logIntentStateUpdated(isRecording: Boolean) = + logger.log( + TAG, + LogLevel.DEBUG, + { bool1 = isRecording }, + { "Update intent has state. isRecording=$bool1" }, + ) + + fun logIntentMissingState() = + logger.log(TAG, LogLevel.ERROR, {}, { "Received update intent with no state" }) + + fun logSentStartIntent() = logger.log(TAG, LogLevel.DEBUG, {}, { "Sent start intent" }) + + fun logPendingIntentCancelled(e: Exception) = + logger.log(TAG, LogLevel.ERROR, {}, { "Pending intent was cancelled" }, e) + + fun logCountdownCancelled() = + logger.log(TAG, LogLevel.DEBUG, {}, { "Record countdown cancelled" }) + + fun logCountdownCancelErrorNoTimer() = + logger.log(TAG, LogLevel.ERROR, {}, { "Couldn't cancel countdown because timer was null" }) + + fun logRecordingStopped() = logger.log(TAG, LogLevel.DEBUG, {}, { "Stopping recording" }) + + fun logRecordingStopErrorNoStopIntent() = + logger.log( + TAG, + LogLevel.ERROR, + {}, + { "Couldn't stop recording because stop intent was null" }, + ) + + fun logRecordingStopError(e: Exception) = + logger.log(TAG, LogLevel.DEBUG, {}, { "Couldn't stop recording" }, e) + + companion object { + private const val TAG = "RecordingController" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt index d7ddc507da82..a830e1bbfe72 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt @@ -16,6 +16,9 @@ package com.android.systemui.screenrecord +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tileimpl.QSTileImpl @@ -53,7 +56,7 @@ interface ScreenRecordModule { @IntoMap @StringKey(SCREEN_RECORD_TILE_SPEC) fun provideScreenRecordAvailabilityInteractor( - impl: ScreenRecordTileDataInteractor + impl: ScreenRecordTileDataInteractor ): QSTileAvailabilityInteractor companion object { @@ -89,5 +92,12 @@ interface ScreenRecordModule { stateInteractor, mapper, ) + + @Provides + @SysUISingleton + @RecordingControllerLog + fun provideRecordingControllerLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("RecordingControllerLog", 50) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 498107624f9b..8e539499cb14 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -137,14 +137,24 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig public void startObserving() { if (!mObserving) { mObserving = true; - mSecureSettings.registerContentObserverForUserSync( - BRIGHTNESS_MODE_URI, - false, this, UserHandle.USER_ALL); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.registerContentObserverForUserAsync( + BRIGHTNESS_MODE_URI, + false, this, UserHandle.USER_ALL); + } else { + mSecureSettings.registerContentObserverForUserSync( + BRIGHTNESS_MODE_URI, + false, this, UserHandle.USER_ALL); + } } } public void stopObserving() { - mSecureSettings.unregisterContentObserverSync(this); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.unregisterContentObserverAsync(this); + } else { + mSecureSettings.unregisterContentObserverSync(this); + } mObserving = false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java index abf258c44556..693cc4a659c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java @@ -485,9 +485,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca final boolean intersectsTopCutout = topDisplayCutout.intersects( width - (windowWidth / 2), 0, width + (windowWidth / 2), topDisplayCutout.bottom); - if (mClingWindow != null && - (windowWidth < 0 || (width > 0 && intersectsTopCutout))) { - final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon); + if (windowWidth < 0 || (width > 0 && intersectsTopCutout)) { + final View iconView = findViewById(R.id.immersive_cling_icon); RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) iconView.getLayoutParams(); lp.topMargin = topDisplayCutout.bottom; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index da89eea35f4c..766c391b14d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -526,7 +526,7 @@ public final class KeyboardShortcuts { keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo( mContext.getString(R.string.keyboard_shortcut_group_applications_assist), assistIcon, - KeyEvent.KEYCODE_UNKNOWN, + KeyEvent.KEYCODE_A, KeyEvent.META_META_ON)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 1789ad6a7216..2081adc076d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -3519,7 +3519,7 @@ public class NotificationStackScrollLayout // Only when scene container is enabled, mark that we are being dragged so that we start // dispatching the rest of the gesture to scene container. void startOverscrollAfterExpanding() { - SceneContainerFlag.isUnexpectedlyInLegacyMode(); + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; getExpandHelper().finishExpanding(); setIsBeingDragged(true); } @@ -3527,7 +3527,7 @@ public class NotificationStackScrollLayout // Only when scene container is enabled, mark that we are being dragged so that we start // dispatching the rest of the gesture to scene container. void startDraggingOnHun() { - SceneContainerFlag.isUnexpectedlyInLegacyMode(); + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; setIsBeingDragged(true); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 726fdee79ef8..a072ea6ec3eb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -2034,6 +2034,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { hunWantsIt = mHeadsUpTouchHelper.onInterceptTouchEvent(ev); if (hunWantsIt) { mView.startDraggingOnHun(); + mHeadsUpManager.unpinAll(true); } } boolean swipeWantsIt = false; 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 eb1f7784f3aa..1a7bc169ea2a 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 @@ -110,6 +110,11 @@ constructor( interactor.setScrolledToTop(scrolledToTop) } + /** Sets whether the heads up notification is animating away. */ + fun setHeadsUpAnimatingAway(animatingAway: Boolean) { + headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway) + } + /** Snooze the currently pinned HUN. */ fun snoozeHun() { headsUpNotificationInteractor.snooze() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 6d76200de34e..6f29f618ee0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -369,10 +369,12 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private void releaseBiometricWakeLock() { if (mWakeLock != null) { + Trace.beginSection("release wake-and-unlock"); mHandler.removeCallbacks(mReleaseBiometricWakeLockRunnable); mLogger.i("releasing biometric wakelock"); mWakeLock.release(); mWakeLock = null; + Trace.endSection(); } } @@ -398,7 +400,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } mWakeLock = mPowerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BIOMETRIC_WAKE_LOCK_NAME); - Trace.beginSection("acquiring wake-and-unlock"); + Trace.beginSection("acquire wake-and-unlock"); mWakeLock.acquire(); Trace.endSection(); mLogger.i("biometric acquired, grabbing biometric wakelock"); @@ -412,14 +414,13 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp public void onBiometricDetected(int userId, BiometricSourceType biometricSourceType, boolean isStrongBiometric) { Trace.beginSection("BiometricUnlockController#onBiometricDetected"); - if (mUpdateMonitor.isGoingToSleep()) { - Trace.endSection(); - return; + if (!mUpdateMonitor.isGoingToSleep()) { + startWakeAndUnlock( + MODE_SHOW_BOUNCER, + BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) + ); } - startWakeAndUnlock( - MODE_SHOW_BOUNCER, - BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) - ); + Trace.endSection(); } @Override @@ -451,6 +452,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } else { mLogger.d("onBiometricUnlocked aborted by bypass controller"); } + Trace.endSection(); } /** @@ -479,6 +481,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp @WakeAndUnlockMode int mode, BiometricUnlockSource biometricUnlockSource ) { + Trace.beginSection("BiometricUnlockController#startWakeAndUnlock"); mLogger.logStartWakeAndUnlock(mode); boolean wasDeviceInteractive = mUpdateMonitor.isDeviceInteractive(); mMode = mode; @@ -501,9 +504,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp "android.policy:BIOMETRIC" ); } - Trace.beginSection("release wake-and-unlock"); releaseBiometricWakeLock(); - Trace.endSection(); }; final boolean wakeInKeyguard = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java index 7f16e18716c4..26bd7ac74d97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java @@ -124,7 +124,6 @@ public class HeadsUpTouchHelper implements Gefingerpoken { mPanel.setHeadsUpDraggingStartingHeight(startHeight); mPanel.startExpand(x, y, true /* startTracking */, startHeight); - // TODO(b/340514839): Figure out where to move this side effect in flexiglass if (!SceneContainerFlag.isEnabled()) { // This call needs to be after the expansion start otherwise we will get a // flicker of one frame as it's not expanded yet. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index 97791acfb43a..316e1f13bc2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -22,6 +22,7 @@ import android.content.res.Resources import android.hardware.biometrics.BiometricSourceType import android.provider.Settings import com.android.app.tracing.ListenersTracing.forEachTraced +import com.android.app.tracing.coroutines.launch import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -159,7 +160,7 @@ class KeyguardBypassController @Inject constructor( } fun listenForQsExpandedChange() = - applicationScope.launch { + applicationScope.launch("listenForQsExpandedChange") { shadeInteractorLazy.get().qsExpansion.map { it > 0f }.distinctUntilChanged() .collect { isQsExpanded -> val changed = qsExpanded != isQsExpanded diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt index bcb613fe2b8c..de76b10db23b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt @@ -276,10 +276,11 @@ constructor( statusBarController } + val isCommunalDismissLaunch = isCommunalWidgetLaunch() && !actuallyShowOverLockscreen // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we // run the animation on the keyguard). The animation will take care of (instantly) // collapsing the shade and hiding the keyguard once it is done. - val collapse = dismissShade && !animate + val collapse = (dismissShade || isCommunalDismissLaunch) && !animate val runnable = Runnable { try { activityTransitionAnimator.startPendingIntentWithAnimation( @@ -338,8 +339,9 @@ constructor( postOnUiThread(delay = 0) { executeRunnableDismissingKeyguard( runnable = runnable, - afterKeyguardGone = willLaunchResolverActivity, dismissShade = collapse, + afterKeyguardGone = willLaunchResolverActivity, + deferred = isCommunalDismissLaunch, willAnimateOnKeyguard = animate, customMessage = customMessage, ) @@ -461,7 +463,9 @@ constructor( override fun onDismiss(): Boolean { if (runnable != null) { if ( - keyguardStateController.isShowing && keyguardStateController.isOccluded + keyguardStateController.isShowing && + keyguardStateController.isOccluded && + !isCommunalWidgetLaunch() ) { statusBarKeyguardViewManagerLazy .get() @@ -473,17 +477,10 @@ constructor( if (dismissShade) { shadeControllerLazy.get().collapseShadeForActivityStart() } - if (communalHub()) { - communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard() - } return deferred } override fun willRunAnimationOnKeyguard(): Boolean { - if (communalHub() && communalSceneInteractor.isIdleOnCommunal.value) { - // Override to false when launching activity over the hub that requires auth - return false - } return willAnimateOnKeyguard } } @@ -639,7 +636,8 @@ constructor( showOverLockscreen: Boolean, ): Boolean { // TODO(b/294418322): always support launch animations when occluded. - val ignoreOcclusion = showOverLockscreen && mediaLockscreenLaunchAnimation() + val ignoreOcclusion = + (showOverLockscreen && mediaLockscreenLaunchAnimation()) || isCommunalWidgetLaunch() if (keyguardStateController.isOccluded && !ignoreOcclusion) { return false } @@ -659,6 +657,12 @@ constructor( return shouldAnimateLaunch(isActivityIntent, false) } + private fun isCommunalWidgetLaunch(): Boolean { + return communalHub() && + communalSceneInteractor.isCommunalVisible.value && + communalSceneInteractor.isLaunchingWidget.value + } + private fun postOnUiThread(delay: Int = 0, runnable: Runnable) { mainExecutor.executeDelayed(runnable, delay.toLong()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 43ab3376e485..40799583a7b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -91,7 +91,11 @@ constructor(dumpManager: DumpManager, } /** Run or delay Runnable for given HeadsUpEntry */ - fun update(entry: HeadsUpEntry?, runnable: Runnable, label: String) { + fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) { + if (runnable == null) { + log { "Runnable is NULL, stop update." } + return + } if (!NotificationThrottleHun.isEnabled) { runnable.run() return @@ -147,7 +151,11 @@ constructor(dumpManager: DumpManager, * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete * all Runnables associated with that entry. */ - fun delete(entry: HeadsUpEntry?, runnable: Runnable, label: String) { + fun delete(entry: HeadsUpEntry?, runnable: Runnable?, label: String) { + if (runnable == null) { + log { "Runnable is NULL, stop delete." } + return + } if (!NotificationThrottleHun.isEnabled) { runnable.run() return diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java index 994a0d0ca76e..7b82b565fc4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java @@ -26,7 +26,6 @@ import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.util.ArrayMap; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -46,11 +45,11 @@ import javax.inject.Inject; /** Platform implementation of the cast controller. **/ @SysUISingleton public class CastControllerImpl implements CastController { - public static final String TAG = "CastController"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String TAG = "CastController"; private final Context mContext; private final PackageManager mPackageManager; + private final CastControllerLogger mLogger; @GuardedBy("mCallbacks") private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final MediaRouter mMediaRouter; @@ -67,20 +66,22 @@ public class CastControllerImpl implements CastController { public CastControllerImpl( Context context, PackageManager packageManager, - DumpManager dumpManager) { + DumpManager dumpManager, + CastControllerLogger logger) { mContext = context; mPackageManager = packageManager; + mLogger = logger; mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); mMediaRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID); mProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); mProjection = mProjectionManager.getActiveProjectionInfo(); mProjectionManager.addCallback(mProjectionCallback, new Handler()); - dumpManager.registerDumpable(TAG, this); - if (DEBUG) Log.d(TAG, "new CastController()"); + dumpManager.registerNormalDumpable(TAG, this); } - public void dump(PrintWriter pw, String[] args) { + @Override + public void dump(PrintWriter pw, @NonNull String[] args) { pw.println("CastController state:"); pw.print(" mDiscovering="); pw.println(mDiscovering); pw.print(" mCallbackRegistered="); pw.println(mCallbackRegistered); @@ -88,7 +89,7 @@ public class CastControllerImpl implements CastController { pw.print(" mRoutes.size="); pw.println(mRoutes.size()); for (int i = 0; i < mRoutes.size(); i++) { final RouteInfo route = mRoutes.valueAt(i); - pw.print(" "); pw.println(routeToString(route)); + pw.print(" "); pw.println(CastControllerLogger.Companion.toLogString(route)); } pw.print(" mProjection="); pw.println(mProjection); } @@ -119,7 +120,7 @@ public class CastControllerImpl implements CastController { synchronized (mDiscoveringLock) { if (mDiscovering == request) return; mDiscovering = request; - if (DEBUG) Log.d(TAG, "setDiscovering: " + request); + mLogger.logDiscovering(request); handleDiscoveryChangeLocked(); } } @@ -166,7 +167,8 @@ public class CastControllerImpl implements CastController { CastDevice.Companion.toCastDevice( mProjection, mContext, - mPackageManager)); + mPackageManager, + mLogger)); } } @@ -177,7 +179,7 @@ public class CastControllerImpl implements CastController { public void startCasting(CastDevice device) { if (device == null || device.getTag() == null) return; final RouteInfo route = (RouteInfo) device.getTag(); - if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route)); + mLogger.logStartCasting(route); mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route); } @@ -185,15 +187,16 @@ public class CastControllerImpl implements CastController { public void stopCasting(CastDevice device) { // TODO(b/332662551): Convert Logcat to LogBuffer. final boolean isProjection = device.getTag() instanceof MediaProjectionInfo; - if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection); + mLogger.logStopCasting(isProjection); if (isProjection) { final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag(); if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) { mProjectionManager.stopActiveProjection(); } else { - Log.w(TAG, "Projection is no longer active: " + projection); + mLogger.logStopCastingNoProjection(projection); } } else { + mLogger.logStopCastingMediaRouter(); mMediaRouter.getFallbackRoute().select(); } } @@ -218,7 +221,7 @@ public class CastControllerImpl implements CastController { } } if (changed) { - if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection); + mLogger.logSetProjection(oldProjection, mProjection); fireOnCastDevicesChanged(); } } @@ -265,42 +268,30 @@ public class CastControllerImpl implements CastController { callback.onCastDevicesChanged(); } - private static String routeToString(RouteInfo route) { - if (route == null) return null; - final StringBuilder sb = new StringBuilder().append(route.getName()).append('/') - .append(route.getDescription()).append('@').append(route.getDeviceAddress()) - .append(",status=").append(route.getStatus()); - if (route.isDefault()) sb.append(",default"); - if (route.isEnabled()) sb.append(",enabled"); - if (route.isConnecting()) sb.append(",connecting"); - if (route.isSelected()) sb.append(",selected"); - return sb.append(",id=").append(route.getTag()).toString(); - } - private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() { @Override public void onRouteAdded(MediaRouter router, RouteInfo route) { - if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route)); + mLogger.logRouteAdded(route); updateRemoteDisplays(); } @Override public void onRouteChanged(MediaRouter router, RouteInfo route) { - if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route)); + mLogger.logRouteChanged(route); updateRemoteDisplays(); } @Override public void onRouteRemoved(MediaRouter router, RouteInfo route) { - if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route)); + mLogger.logRouteRemoved(route); updateRemoteDisplays(); } @Override public void onRouteSelected(MediaRouter router, int type, RouteInfo route) { - if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route)); + mLogger.logRouteSelected(route, type); updateRemoteDisplays(); } @Override public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) { - if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route)); + mLogger.logRouteUnselected(route, type); updateRemoteDisplays(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt new file mode 100644 index 000000000000..9a3a244c6157 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt @@ -0,0 +1,141 @@ +/* + * 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.statusbar.policy + +import android.media.MediaRouter.RouteInfo +import android.media.projection.MediaProjectionInfo +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.MessageInitializer +import com.android.systemui.log.core.MessagePrinter +import com.android.systemui.statusbar.policy.dagger.CastControllerLog +import javax.inject.Inject + +/** Helper class for logging events to [CastControllerLog] from Java. */ +@SysUISingleton +class CastControllerLogger +@Inject +constructor( + @CastControllerLog val logger: LogBuffer, +) { + /** Passthrough to [logger]. */ + inline fun log( + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + ) { + logger.log(tag, level, messageInitializer, messagePrinter, exception) + } + + fun logDiscovering(isDiscovering: Boolean) = + logger.log(TAG, LogLevel.DEBUG, { bool1 = isDiscovering }, { "setDiscovering: $bool1" }) + + fun logStartCasting(route: RouteInfo) = + logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "startCasting: $str1" }) + + fun logStopCasting(isProjection: Boolean) = + logger.log( + TAG, + LogLevel.DEBUG, + { bool1 = isProjection }, + { "stopCasting. isProjection=$bool1" }, + ) + + fun logStopCastingNoProjection(projection: MediaProjectionInfo) = + logger.log( + TAG, + LogLevel.WARNING, + { str1 = projection.toString() }, + { "stopCasting failed because projection is no longer active: $str1" }, + ) + + fun logStopCastingMediaRouter() = + logger.log( + TAG, + LogLevel.DEBUG, + {}, + { "stopCasting is selecting fallback route in MediaRouter" }, + ) + + fun logSetProjection(oldInfo: MediaProjectionInfo?, newInfo: MediaProjectionInfo?) = + logger.log( + TAG, + LogLevel.DEBUG, + { + str1 = oldInfo.toString() + str2 = newInfo.toString() + }, + { "setProjection: $str1 -> $str2" }, + ) + + fun logRouteAdded(route: RouteInfo) = + logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteAdded: $str1" }) + + fun logRouteChanged(route: RouteInfo) = + logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteChanged: $str1" }) + + fun logRouteRemoved(route: RouteInfo) = + logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteRemoved: $str1" }) + + fun logRouteSelected(route: RouteInfo, type: Int) = + logger.log( + TAG, + LogLevel.DEBUG, + { + str1 = route.toLogString() + int1 = type + }, + { "onRouteSelected($int1): $str1" }, + ) + + fun logRouteUnselected(route: RouteInfo, type: Int) = + logger.log( + TAG, + LogLevel.DEBUG, + { + str1 = route.toLogString() + int1 = type + }, + { "onRouteUnselected($int1): $str1" }, + ) + + companion object { + @JvmStatic + fun RouteInfo?.toLogString(): String? { + if (this == null) return null + val sb = + StringBuilder() + .append(name) + .append('/') + .append(description) + .append('@') + .append(deviceAddress) + .append(",status=") + .append(status) + if (isDefault) sb.append(",default") + if (isEnabled) sb.append(",enabled") + if (isConnecting) sb.append(",connecting") + if (isSelected) sb.append(",selected") + return sb.append(",id=").append(this.tag).toString() + } + + private const val TAG = "CastController" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt index 68edd758808c..a787f7e09f0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt @@ -20,7 +20,7 @@ import android.content.pm.PackageManager import android.media.MediaRouter import android.media.projection.MediaProjectionInfo import android.text.TextUtils -import android.util.Log +import com.android.systemui.log.core.LogLevel import com.android.systemui.res.R import com.android.systemui.util.Utils @@ -64,11 +64,12 @@ data class CastDevice( /** Creates a [CastDevice] based on the provided information from MediaProjection. */ fun MediaProjectionInfo.toCastDevice( context: Context, - packageManager: PackageManager + packageManager: PackageManager, + logger: CastControllerLogger, ): CastDevice { return CastDevice( id = this.packageName, - name = getAppName(this.packageName, packageManager), + name = getAppName(this.packageName, packageManager, logger), description = context.getString(R.string.quick_settings_casting), state = CastState.Connected, tag = this, @@ -76,7 +77,11 @@ data class CastDevice( ) } - private fun getAppName(packageName: String, packageManager: PackageManager): String { + private fun getAppName( + packageName: String, + packageManager: PackageManager, + logger: CastControllerLogger, + ): String { if (Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)) { return "" } @@ -86,9 +91,20 @@ data class CastDevice( if (!TextUtils.isEmpty(label)) { return label.toString() } - Log.w(CastControllerImpl.TAG, "No label found for package: $packageName") + logger.log( + "#getAppName", + LogLevel.WARNING, + { str1 = packageName }, + { "No label found for package: $str1" }, + ) } catch (e: PackageManager.NameNotFoundException) { - Log.w(CastControllerImpl.TAG, "Error getting appName for package: $packageName", e) + logger.log( + "#getAppName", + LogLevel.WARNING, + { str1 = packageName }, + { "Error getting appName for package=$str1" }, + e, + ) } return packageName } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt new file mode 100644 index 000000000000..23aade6e6c17 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.dagger + +import javax.inject.Qualifier + +/** + * Logs for cast events. See [com.android.systemui.statusbar.policy.CastControllerImpl] and + * [com.android.systemui.statusbar.policy.CastControllerLogger]. + */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CastControllerLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index e08e4d70a18e..71bcdfcba049 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -228,4 +228,12 @@ public interface StatusBarPolicyModule { static LogBuffer provideBatteryControllerLog(LogBufferFactory factory) { return factory.create(BatteryControllerLogger.TAG, 30); } + + /** Provides a log buffer for CastControllerImpl */ + @Provides + @SysUISingleton + @CastControllerLog + static LogBuffer provideCastControllerLog(LogBufferFactory factory) { + return factory.create("CastControllerLog", 50); + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 025354b51133..848a6e691082 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -16,12 +16,12 @@ package com.android.systemui.util.settings import android.annotation.UserIdInt +import android.annotation.WorkerThread import android.content.ContentResolver import android.database.ContentObserver import android.net.Uri import android.os.UserHandle import android.provider.Settings.SettingNotFoundException -import androidx.annotation.WorkerThread import com.android.app.tracing.TraceUtils.trace import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat @@ -67,6 +67,7 @@ interface UserSettingsProxy : SettingsProxy { } else userTracker.userId } + @WorkerThread override fun registerContentObserverSync(uri: Uri, settingsObserver: ContentObserver) { registerContentObserverForUserSync(uri, settingsObserver, userId) } @@ -83,6 +84,7 @@ interface UserSettingsProxy : SettingsProxy { } /** Convenience wrapper around [ContentResolver.registerContentObserver].' */ + @WorkerThread override fun registerContentObserverSync( uri: Uri, notifyForDescendants: Boolean, @@ -120,6 +122,7 @@ interface UserSettingsProxy : SettingsProxy { * * Implicitly calls [getUriFor] on the passed in name. */ + @WorkerThread fun registerContentObserverForUserSync( name: String, settingsObserver: ContentObserver, @@ -160,6 +163,7 @@ interface UserSettingsProxy : SettingsProxy { } /** Convenience wrapper around [ContentResolver.registerContentObserver] */ + @WorkerThread fun registerContentObserverForUserSync( uri: Uri, settingsObserver: ContentObserver, @@ -222,6 +226,7 @@ interface UserSettingsProxy : SettingsProxy { * * Implicitly calls [getUriFor] on the passed in name. */ + @WorkerThread fun registerContentObserverForUserSync( name: String, notifyForDescendants: Boolean, @@ -281,6 +286,7 @@ interface UserSettingsProxy : SettingsProxy { } /** Convenience wrapper around [ContentResolver.registerContentObserver] */ + @WorkerThread fun registerContentObserverForUserSync( uri: Uri, notifyForDescendants: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index de8b9b1e7e3c..eb2f71a1cd7d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -73,10 +73,19 @@ interface AudioModule { @Provides @SysUISingleton fun provideAudioSharingRepository( + @Application context: Context, + contentResolver: ContentResolver, localBluetoothManager: LocalBluetoothManager?, + @Application coroutineScope: CoroutineScope, @Background coroutineContext: CoroutineContext, ): AudioSharingRepository = - AudioSharingRepositoryImpl(localBluetoothManager, coroutineContext) + AudioSharingRepositoryImpl( + context, + contentResolver, + localBluetoothManager, + coroutineScope, + coroutineContext + ) @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt new file mode 100644 index 000000000000..29040923a2cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt @@ -0,0 +1,35 @@ +/* + * 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.volume.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor +import com.android.systemui.volume.domain.interactor.AudioSharingInteractorEmptyImpl +import dagger.Module +import dagger.Provides + +/** Dagger module for empty audio sharing impl for unnecessary volume overlay */ +@Module +interface AudioSharingEmptyImplModule { + + companion object { + @Provides + @SysUISingleton + fun provideAudioSharingInteractor(): AudioSharingInteractor = + AudioSharingInteractorEmptyImpl() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt new file mode 100644 index 000000000000..9f1e60e855e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt @@ -0,0 +1,40 @@ +/* + * 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.volume.dagger + +import com.android.settingslib.volume.data.repository.AudioSharingRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor +import com.android.systemui.volume.domain.interactor.AudioSharingInteractorImpl +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineScope + +/** Dagger module for audio sharing code in the volume package */ +@Module +interface AudioSharingModule { + + companion object { + @Provides + @SysUISingleton + fun provideAudioSharingInteractor( + @Application coroutineScope: CoroutineScope, + repository: AudioSharingRepository + ): AudioSharingInteractor = AudioSharingInteractorImpl(coroutineScope, repository) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 5420988b8faf..ebb9ce9909bd 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -59,6 +59,7 @@ import dagger.multibindings.IntoSet; @Module( includes = { AudioModule.class, + AudioSharingModule.class, AncModule.class, CaptioningModule.class, MediaDevicesModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt new file mode 100644 index 000000000000..4d29788edf68 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.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.volume.domain.interactor + +import android.bluetooth.BluetoothCsipSetCoordinator +import androidx.annotation.IntRange +import com.android.settingslib.volume.data.repository.AudioSharingRepository +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.launch + +interface AudioSharingInteractor { + /** Audio sharing secondary headset volume changes. */ + val volume: Flow<Int?> + + /** Audio sharing secondary headset min volume. */ + val volumeMin: Int + + /** Audio sharing secondary headset max volume. */ + val volumeMax: Int + + /** Set the volume of the secondary headset in audio sharing. */ + fun setStreamVolume( + @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) + level: Int + ) +} + +@SysUISingleton +class AudioSharingInteractorImpl +@Inject +constructor( + @Application private val coroutineScope: CoroutineScope, + private val audioSharingRepository: AudioSharingRepository +) : AudioSharingInteractor { + + override val volume: Flow<Int?> = + combine(audioSharingRepository.secondaryGroupId, audioSharingRepository.volumeMap) { + secondaryGroupId, + volumeMap -> + if (secondaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) null + else volumeMap.getOrDefault(secondaryGroupId, DEFAULT_VOLUME) + } + + override val volumeMin: Int = AUDIO_SHARING_VOLUME_MIN + + override val volumeMax: Int = AUDIO_SHARING_VOLUME_MAX + + override fun setStreamVolume(level: Int) { + coroutineScope.launch { audioSharingRepository.setSecondaryVolume(level) } + } + + private companion object { + const val DEFAULT_VOLUME = 20 + } +} + +@SysUISingleton +class AudioSharingInteractorEmptyImpl : AudioSharingInteractor { + override val volume: Flow<Int?> = emptyFlow() + override val volumeMin: Int = EMPTY_VOLUME + override val volumeMax: Int = EMPTY_VOLUME + + override fun setStreamVolume(level: Int) {} + + private companion object { + const val EMPTY_VOLUME = 0 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt new file mode 100644 index 000000000000..baef620ad556 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt @@ -0,0 +1,221 @@ +/* + * 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.biometrics + +import android.hardware.biometrics.BiometricFaceConstants +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@TestableLooper.RunWithLooper +class FaceHelpMessageDebouncerTest : SysuiTestCase() { + private lateinit var underTest: FaceHelpMessageDebouncer + private val window = 9L + private val startWindow = 4L + private val shownFaceMessageFrequencyBoost = 2 + + @Before + fun setUp() { + underTest = + FaceHelpMessageDebouncer( + window = window, + startWindow = startWindow, + shownFaceMessageFrequencyBoost = shownFaceMessageFrequencyBoost, + ) + } + + @Test + fun getMessageBeforeStartWindow_null() { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "testTooClose", + 0 + ) + ) + assertThat(underTest.getMessageToShow(0)).isNull() + } + + @Test + fun getMessageAfterStartWindow() { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + + assertThat(underTest.getMessageToShow(startWindow)?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) + assertThat(underTest.getMessageToShow(startWindow)?.msg).isEqualTo("tooClose") + } + + @Test + fun getMessageAfterMessagesCleared_null() { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.startNewFaceAuthSession(0) + + assertThat(underTest.getMessageToShow(startWindow)).isNull() + } + + @Test + fun messagesBeforeWindowRemoved() { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + window - 1 + ) + ) + val lastMessage = + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + window + ) + underTest.addMessage(lastMessage) + + assertThat(underTest.getMessageToShow(window + 1)).isEqualTo(lastMessage) + } + + @Test + fun getMessageTieGoesToMostRecent() { + for (i in 1..window step 2) { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + i + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + i + 1 + ) + ) + } + + assertThat(underTest.getMessageToShow(window)?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT) + assertThat(underTest.getMessageToShow(window)?.msg).isEqualTo("tooBright") + } + + @Test + fun boostCurrentlyShowingMessage() { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + 0 + ) + ) + + val lastMessageShown = underTest.getMessageToShow(startWindow) + assertThat(lastMessageShown?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT) + + for (i in 1..<shownFaceMessageFrequencyBoost) { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + startWindow + ) + ) + } + + // although technically there's a different msgId with a higher frequency count now, the + // shownFaceMessageFrequencyBoost causes the last message shown to get a "boost" + // to keep showing + assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(lastMessageShown) + } + + @Test + fun overcomeBoostedCurrentlyShowingMessage() { + // Comments are assuming shownFaceMessageFrequencyBoost = 2 + // [B], weights: B=1 + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + 0 + ) + ) + + // [B], showing messageB, weights: B=3 + val messageB = underTest.getMessageToShow(startWindow) + + // [B, C, C], showing messageB, weights: B=3, C=2 + for (i in 1..shownFaceMessageFrequencyBoost) { + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + startWindow + ) + ) + } + // messageB is getting boosted to continue to show + assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(messageB) + + // receive one more FACE_ACQUIRED_TOO_CLOSE acquired info to pass the boost + // [C, C, C], showing messageB, weights: B=2, C=3 + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + startWindow + ) + ) + + // Now FACE_ACQUIRED_TOO_CLOSE has surpassed the boosted messageB frequency + // [C, C, C], showing messageC, weights: C=5 + assertThat(underTest.getMessageToShow(startWindow)?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt index 431fef65e95b..6fd866066879 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt @@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.FaceHelpMessageDebouncer import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.biometrics.domain.faceHelpMessageDeferral import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -45,6 +46,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -264,11 +266,20 @@ class BiometricMessageInteractorTest : SysuiTestCase() { biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - // WHEN authentication status help + // WHEN authentication status help past debouncer + faceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus( + msg = "Move left", + msgId = FACE_ACQUIRED_TOO_RIGHT, + createdAt = 0L, + ) + ) + runCurrent() faceAuthRepository.setAuthenticationStatus( HelpFaceAuthenticationStatus( msg = "Move left", msgId = FACE_ACQUIRED_TOO_RIGHT, + createdAt = FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS, ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 529cd6ea8ddc..0b7a3ed3a7c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -90,6 +90,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager + private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor @Before fun setup() { @@ -112,6 +113,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { powerInteractor, fakeBiometricSettingsRepository, trustManager, + deviceEntryFaceAuthStatusInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt new file mode 100644 index 000000000000..6022d9cfcbfd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt @@ -0,0 +1,160 @@ +/* + * 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.deviceentry.domain.interactor + +import android.hardware.biometrics.BiometricFaceConstants +import android.hardware.face.FaceManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.FaceHelpMessageDebouncer.Companion.DEFAULT_WINDOW_MS +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus +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.deviceentry.shared.model.SuccessFaceAuthenticationStatus +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntryFaceAuthStatusInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope: TestScope = kosmos.testScope + private lateinit var underTest: DeviceEntryFaceAuthStatusInteractor + private val ignoreHelpMessageId = 1 + + @Before + fun setup() { + overrideResource( + R.array.config_face_acquire_device_entry_ignorelist, + intArrayOf(ignoreHelpMessageId) + ) + underTest = kosmos.deviceEntryFaceAuthStatusInteractor + } + + @Test + fun successAuthenticationStatus() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + val successStatus = + SuccessFaceAuthenticationStatus( + successResult = mock(FaceManager.AuthenticationResult::class.java) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(successStatus) + assertThat(authenticationStatus).isEqualTo(successStatus) + } + + @Test + fun acquiredFaceAuthenticationStatus() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + val acquiredStatus = AcquiredFaceAuthenticationStatus(acquiredInfo = 0) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(acquiredStatus) + assertThat(authenticationStatus).isEqualTo(acquiredStatus) + } + + @Test + fun failedFaceAuthenticationStatus() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + val failedStatus = FailedFaceAuthenticationStatus() + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(failedStatus) + assertThat(authenticationStatus).isEqualTo(failedStatus) + } + + @Test + fun errorFaceAuthenticationStatus() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + val errorStatus = ErrorFaceAuthenticationStatus(0, "test") + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(errorStatus) + assertThat(authenticationStatus).isEqualTo(errorStatus) + } + + @Test + fun firstHelpFaceAuthenticationStatus_noUpdate() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + AcquiredFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_START, + createdAt = 0 + ) + ) + val helpMessage = HelpFaceAuthenticationStatus(0, "test", 1) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage) + assertThat(authenticationStatus).isNull() + } + + @Test + fun helpFaceAuthenticationStatus_afterWindow() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(0, "test1", 0) + ) + runCurrent() + val helpMessage = HelpFaceAuthenticationStatus(0, "test2", DEFAULT_WINDOW_MS) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage) + runCurrent() + assertThat(authenticationStatus).isEqualTo(helpMessage) + } + + @Test + fun helpFaceAuthenticationStatus_onlyIgnoredHelpMessages_afterWindow() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", 0) + ) + runCurrent() + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS) + ) + runCurrent() + assertThat(authenticationStatus).isNull() + } + + @Test + fun helpFaceAuthenticationStatus_afterWindow_onIgnoredMessage_showsOtherMessageInstead() = + testScope.runTest { + val authenticationStatus by collectLastValue(underTest.authenticationStatus) + val validHelpMessage = HelpFaceAuthenticationStatus(0, "validHelpMsg", 0) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(validHelpMessage) + runCurrent() + // help message that should be ignored + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS) + ) + runCurrent() + assertThat(authenticationStatus).isEqualTo(validHelpMessage) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index e2cca3873bf7..ae635b8cbfd4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -41,7 +41,6 @@ import android.media.AudioManager; import android.os.Handler; import android.os.UserManager; import android.provider.Settings; -import android.service.dreams.IDreamManager; import android.testing.TestableLooper; import android.view.GestureDetector; import android.view.IWindowManager; @@ -106,7 +105,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs; @Mock private AudioManager mAudioManager; - @Mock private IDreamManager mDreamManager; @Mock private DevicePolicyManager mDevicePolicyManager; @Mock private LockPatternUtils mLockPatternUtils; @Mock private BroadcastDispatcher mBroadcastDispatcher; @@ -165,7 +163,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext, mWindowManagerFuncs, mAudioManager, - mDreamManager, mDevicePolicyManager, mLockPatternUtils, mBroadcastDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt index 5db898115f2d..785d5a8e6184 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken @@ -273,6 +274,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { applicationScope = kosmos.applicationCoroutineScope, backgroundDispatcher = kosmos.testDispatcher, mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, + logger = logcatLogBuffer("TestMediaProjection"), ) val state by collectLastValue(repoWithTimingControl.mediaProjectionState) diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index b169cc12f08a..b4db6da2000a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -27,6 +27,7 @@ import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.jank.Cuj import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase @@ -63,7 +64,7 @@ class BackPanelControllerTest : SysuiTestCase() { private var triggerThreshold: Float = 0.0f private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop @Mock private lateinit var vibratorHelper: VibratorHelper - @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } @@ -78,7 +79,7 @@ class BackPanelControllerTest : SysuiTestCase() { mBackPanelController = BackPanelController( context, - windowManager, + viewCaptureAwareWindowManager, ViewConfiguration.get(context), Handler.createAsync(testableLooper.looper), systemClock, diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index 98ff6c90b4b2..45d77f6d76cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -29,6 +29,8 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; import static com.android.systemui.navigationbar.views.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS; +import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS; +import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.google.common.truth.Truth.assertThat; @@ -38,6 +40,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -70,6 +73,7 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -162,6 +166,8 @@ public class NavigationBarTest extends SysuiTestCase { @Mock ButtonDispatcher mImeSwitchButton; @Mock + KeyButtonView mImeSwitchButtonView; + @Mock ButtonDispatcher mBackButton; @Mock NavigationBarTransitions mNavigationBarTransitions; @@ -433,6 +439,45 @@ public class NavigationBarTest extends SysuiTestCase { } @Test + public void testImeSwitcherClick() { + mNavigationBar.init(); + mNavigationBar.onViewAttached(); + mNavigationBar.onImeSwitcherClick(mImeSwitchButtonView); + + verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_TAP); + verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); + if (Flags.imeSwitcherRevamp()) { + verify(mInputMethodManager) + .onImeSwitchButtonClickFromSystem(mNavigationBar.mDisplayId); + verify(mInputMethodManager, never()).showInputMethodPickerFromSystem( + anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */); + } else { + verify(mInputMethodManager, never()) + .onImeSwitchButtonClickFromSystem(anyInt() /* displayId */); + verify(mInputMethodManager).showInputMethodPickerFromSystem( + true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId); + } + } + + @Test + public void testImeSwitcherLongClick() { + mNavigationBar.init(); + mNavigationBar.onViewAttached(); + mNavigationBar.onImeSwitcherLongClick(mImeSwitchButtonView); + + verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_TAP); + if (Flags.imeSwitcherRevamp()) { + verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mInputMethodManager).showInputMethodPickerFromSystem( + true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId); + } else { + verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mInputMethodManager, never()).showInputMethodPickerFromSystem( + anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */); + } + } + + @Test public void testRegisteredWithUserTracker() { mNavigationBar.init(); mNavigationBar.onViewAttached(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java index 5273495bec2f..eea02eec7099 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java @@ -61,6 +61,7 @@ import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.wifi.WifiUtils; @@ -160,7 +161,7 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { @Mock InternetDialogController.InternetDialogCallback mInternetDialogCallback; @Mock - private WindowManager mWindowManager; + private ViewCaptureAwareWindowManager mWindowManager; @Mock private ToastFactory mToastFactory; @Mock @@ -232,8 +233,9 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { mSubscriptionManager, mTelephonyManager, mWifiManager, mConnectivityManager, mHandler, mExecutor, mBroadcastDispatcher, mock(KeyguardUpdateMonitor.class), mGlobalSettings, mKeyguardStateController, - mWindowManager, mToastFactory, mWorkerHandler, mCarrierConfigTracker, - mLocationController, mDialogTransitionAnimator, mWifiStateWorker, mFlags); + mWindowManager, mToastFactory, mWorkerHandler, + mCarrierConfigTracker, mLocationController, mDialogTransitionAnimator, + mWifiStateWorker, mFlags); mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, mInternetDialogController.mOnSubscriptionsChangedListener); mInternetDialogController.onStart(mInternetDialogCallback, true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index 2444af7204e7..477c50b58519 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -18,6 +18,8 @@ package com.android.systemui.screenrecord; import static android.os.Process.myUid; +import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; @@ -69,10 +71,6 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -/** - * Tests for exception handling and bitmap configuration in adding smart actions to Screenshot - * Notification. - */ public class RecordingControllerTest extends SysuiTestCase { private static final int TEST_USER_ID = 12345; @@ -146,6 +144,7 @@ public class RecordingControllerTest extends SysuiTestCase { mFeatureFlags, () -> mDevicePolicyResolver, mUserTracker, + new RecordingControllerLogger(logcatLogBuffer("RecordingControllerTest")), mMediaProjectionMetricsLogger, mScreenCaptureDisabledDialogDelegate, mScreenRecordDialogFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 10d07a0ce004..5052a008af68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -35,8 +35,8 @@ import android.os.Handler; import android.os.PowerManager; import android.provider.Settings; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; @@ -53,6 +53,7 @@ import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Assert; import org.junit.Before; @@ -114,6 +115,7 @@ public class DozeParametersTest extends SysuiTestCase { .thenReturn(mFoldAodAnimationController); when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); + SecureSettings secureSettings = new FakeSettings(); mDozeParameters = new DozeParameters( mContext, mHandler, @@ -132,7 +134,7 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController, mUserTracker, mDozeInteractor, - new FakeSettings() + secureSettings ); verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java index 59b20c824739..627463b45982 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java @@ -1,6 +1,8 @@ package com.android.systemui.statusbar.policy; +import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; + import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -58,7 +60,8 @@ public class CastControllerImplTest extends SysuiTestCase { mController = new CastControllerImpl( mContext, mock(PackageManager.class), - mock(DumpManager.class)); + mock(DumpManager.class), + new CastControllerLogger(logcatLogBuffer("CastControllerImplTest"))); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt index 03ad66cd4cce..16061df1fa89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt @@ -25,6 +25,7 @@ import android.media.MediaRouter import android.media.projection.MediaProjectionInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R import com.android.systemui.statusbar.policy.CastDevice.Companion.toCastDevice import com.google.common.truth.Truth.assertThat @@ -40,6 +41,7 @@ import org.mockito.kotlin.whenever class CastDeviceTest : SysuiTestCase() { private val mockAppInfo = mock<ApplicationInfo>().apply { whenever(this.loadLabel(any())).thenReturn("") } + private val logger = CastControllerLogger(logcatLogBuffer("CastDeviceTest")) private val packageManager = mock<PackageManager>().apply { @@ -322,7 +324,7 @@ class CastDeviceTest : SysuiTestCase() { whenever(this.packageName).thenReturn("fake.package") } - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.id).isEqualTo("fake.package") } @@ -334,7 +336,7 @@ class CastDeviceTest : SysuiTestCase() { whenever(this.packageName).thenReturn(HEADLESS_REMOTE_PACKAGE) } - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.name).isEmpty() } @@ -349,7 +351,7 @@ class CastDeviceTest : SysuiTestCase() { whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>())) .thenThrow(PackageManager.NameNotFoundException()) - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.name).isEqualTo(NORMAL_PACKAGE) } @@ -366,7 +368,7 @@ class CastDeviceTest : SysuiTestCase() { whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>())) .thenReturn(appInfo) - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.name).isEqualTo(NORMAL_PACKAGE) } @@ -383,7 +385,7 @@ class CastDeviceTest : SysuiTestCase() { whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>())) .thenReturn(appInfo) - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.name).isEqualTo("Valid App Name") } @@ -392,7 +394,7 @@ class CastDeviceTest : SysuiTestCase() { fun projectionToCastDevice_descriptionIsCasting() { val projection = mockProjectionInfo() - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.description).isEqualTo(context.getString(R.string.quick_settings_casting)) } @@ -401,7 +403,7 @@ class CastDeviceTest : SysuiTestCase() { fun projectionToCastDevice_stateIsConnected() { val projection = mockProjectionInfo() - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.state).isEqualTo(CastDevice.CastState.Connected) } @@ -410,7 +412,7 @@ class CastDeviceTest : SysuiTestCase() { fun projectionToCastDevice_tagIsProjection() { val projection = mockProjectionInfo() - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.tag).isEqualTo(projection) } @@ -419,7 +421,7 @@ class CastDeviceTest : SysuiTestCase() { fun projectionToCastDevice_originIsMediaProjection() { val projection = mockProjectionInfo() - val device = projection.toCastDevice(context, packageManager) + val device = projection.toCastDevice(context, packageManager, logger) assertThat(device.origin).isEqualTo(CastDevice.CastOrigin.MediaProjection) } 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 index 4b6441628500..de5f0f3734c6 100644 --- 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 @@ -24,7 +24,7 @@ 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.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel @@ -44,7 +44,7 @@ val Kosmos.bouncerMessageViewModel by clock = systemClock, biometricMessageInteractor = biometricMessageInteractor, faceAuthInteractor = deviceEntryFaceAuthInteractor, - deviceEntryInteractor = deviceEntryInteractor, + deviceUnlockedInteractor = deviceUnlockedInteractor, fingerprintInteractor = deviceEntryFingerprintAuthInteractor, flags = composeBouncerFlags, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 25e7729e0eb3..045bd5d286df 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -30,16 +30,10 @@ class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { private val _isBypassEnabled = MutableStateFlow(false) override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled - var userPresentCount = 0 - override suspend fun isLockscreenEnabled(): Boolean { return isLockscreenEnabled } - override suspend fun reportUserPresent() { - userPresentCount++ - } - fun setLockscreenEnabled(isLockscreenEnabled: Boolean) { this.isLockscreenEnabled = isLockscreenEnabled } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt index a8fc27a7da4e..b9be04dc0a32 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt @@ -57,5 +57,6 @@ val Kosmos.deviceEntryFaceAuthInteractor by powerInteractor = powerInteractor, biometricSettingsRepository = biometricSettingsRepository, trustManager = trustManager, + deviceEntryFaceAuthStatusInteractor = deviceEntryFaceAuthStatusInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt new file mode 100644 index 000000000000..66d3709d14dc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.deviceentry.domain.interactor + +import android.content.res.mainResources +import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.deviceEntryFaceAuthStatusInteractor by + Kosmos.Fixture { + DeviceEntryFaceAuthStatusInteractor( + repository = deviceEntryFaceAuthRepository, + resources = mainResources, + applicationScope = applicationCoroutineScope, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 120086686282..caa6e99d58cb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -19,8 +19,6 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository -import com.android.systemui.flags.fakeSystemPropertiesHelper -import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -34,12 +32,7 @@ val Kosmos.deviceEntryInteractor by repository = deviceEntryRepository, authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - faceAuthInteractor = deviceEntryFaceAuthInteractor, - fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, - biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, - trustInteractor = trustInteractor, deviceUnlockedInteractor = deviceUnlockedInteractor, - systemPropertiesHelper = fakeSystemPropertiesHelper, alternateBouncerInteractor = alternateBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt index 14210bc8d15c..1ed10fbe94c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.flags.systemPropertiesHelper import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -33,5 +35,7 @@ val Kosmos.deviceUnlockedInteractor by Fixture { faceAuthInteractor = deviceEntryFaceAuthInteractor, fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, powerInteractor = powerInteractor, + biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, + systemPropertiesHelper = fakeSystemPropertiesHelper, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt new file mode 100644 index 000000000000..f73f43dc53fe --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright 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.education.data.repository + +import com.android.systemui.kosmos.Kosmos +import java.time.Instant + +var Kosmos.contextualEducationRepository: ContextualEducationRepository by + Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt new file mode 100644 index 000000000000..5410882c9283 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt @@ -0,0 +1,53 @@ +/* + * Copyright 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.education.data.repository + +import com.android.systemui.education.data.model.GestureEduModel +import com.android.systemui.shared.education.GestureType +import java.time.Clock +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeContextualEducationRepository(private val clock: Clock) : ContextualEducationRepository { + + private val userGestureMap = mutableMapOf<Int, GestureEduModel>() + private val _gestureEduModels = MutableStateFlow(GestureEduModel()) + private val gestureEduModelsFlow = _gestureEduModels.asStateFlow() + + override fun setUser(userId: Int) { + if (!userGestureMap.contains(userId)) { + userGestureMap[userId] = GestureEduModel() + } + _gestureEduModels.value = userGestureMap[userId]!! + } + + override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> { + return gestureEduModelsFlow + } + + override suspend fun incrementSignalCount(gestureType: GestureType) { + _gestureEduModels.value = + GestureEduModel( + signalCount = _gestureEduModels.value.signalCount + 1, + ) + } + + override suspend fun updateShortcutTriggerTime(gestureType: GestureType) { + _gestureEduModels.value = GestureEduModel(lastShortcutTriggeredTime = clock.instant()) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt new file mode 100644 index 000000000000..513c14381997 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt @@ -0,0 +1,37 @@ +/* + * Copyright 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.education.data.repository + +import java.time.Clock +import java.time.Instant +import java.time.ZoneId + +class FakeEduClock(private val base: Instant) : Clock() { + private val zone: ZoneId = ZoneId.of("UTC") + + override fun instant(): Instant { + return base + } + + override fun withZone(zoneId: ZoneId?): Clock { + return FakeEduClock(base) + } + + override fun getZone(): ZoneId { + return zone + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..b5f0b897deba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +var Kosmos.dreamingToAodTransitionViewModel by Fixture { + DreamingToAodTransitionViewModel( + deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + animationFlow = keyguardTransitionAnimationFlow, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 2567ffee9be8..3c5baa557513 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -46,6 +46,7 @@ val Kosmos.keyguardRootViewModel by Fixture { dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, + dreamingToAodTransitionViewModel = dreamingToAodTransitionViewModel, dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel, dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt index 81ba77a341b7..04122742bb0d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt @@ -21,6 +21,7 @@ import android.os.Handler import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager @@ -37,5 +38,6 @@ val Kosmos.realMediaProjectionRepository by tasksRepository = activityTaskManagerTasksRepository, backgroundDispatcher = testDispatcher, mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, + logger = logcatLogBuffer("TestMediaProjection"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt index 327e1b5df2ac..d391750a2612 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt @@ -16,16 +16,40 @@ package com.android.systemui.volume.data.repository +import androidx.annotation.IntRange import com.android.settingslib.volume.data.repository.AudioSharingRepository +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX +import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN +import com.android.settingslib.volume.data.repository.GroupIdToVolumes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class FakeAudioSharingRepository : AudioSharingRepository { private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false) + private val mutableSecondaryGroupId: MutableStateFlow<Int> = + MutableStateFlow(TEST_GROUP_ID_INVALID) + private val mutableVolumeMap: MutableStateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap()) override val inAudioSharing: Flow<Boolean> = mutableInAudioSharing + override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId + override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap + + override suspend fun setSecondaryVolume(volume: Int) {} fun setInAudioSharing(state: Boolean) { mutableInAudioSharing.value = state } + + fun setSecondaryGroupId(groupId: Int) { + mutableSecondaryGroupId.value = groupId + } + + fun setVolumeMap(volumeMap: GroupIdToVolumes) { + mutableVolumeMap.value = volumeMap + } + + private companion object { + const val TEST_GROUP_ID_INVALID = -1 + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt new file mode 100644 index 000000000000..03981bbef444 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.volume.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.data.repository.audioSharingRepository + +val Kosmos.audioSharingInteractor by + Kosmos.Fixture { + AudioSharingInteractorImpl( + applicationCoroutineScope, + audioSharingRepository, + ) + } diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java index e031eb27513b..8a1fe62db4e1 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java @@ -30,7 +30,6 @@ public final class Os { return RavenwoodRuntimeNative.lseek(fd, offset, whence); } - public static FileDescriptor[] pipe2(int flags) throws ErrnoException { return RavenwoodRuntimeNative.pipe2(flags); } @@ -42,4 +41,16 @@ public final class Os { public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException { return RavenwoodRuntimeNative.fcntlInt(fd, cmd, arg); } + + public static StructStat fstat(FileDescriptor fd) throws ErrnoException { + return RavenwoodRuntimeNative.fstat(fd); + } + + public static StructStat lstat(String path) throws ErrnoException { + return RavenwoodRuntimeNative.lstat(path); + } + + public static StructStat stat(String path) throws ErrnoException { + return RavenwoodRuntimeNative.stat(path); + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java new file mode 100644 index 000000000000..a8b1fca464dd --- /dev/null +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.system; + +import libcore.util.Objects; + +/** + * File information returned by {@link Os#fstat}, {@link Os#lstat}, and {@link Os#stat}. + * Corresponds to C's {@code struct stat} from {@code <stat.h>}. + */ +public final class StructStat { + /** Device ID of device containing file. */ + public final long st_dev; /*dev_t*/ + + /** File serial number (inode). */ + public final long st_ino; /*ino_t*/ + + /** Mode (permissions) of file. */ + public final int st_mode; /*mode_t*/ + + /** Number of hard links to the file. */ + public final long st_nlink; /*nlink_t*/ + + /** User ID of file. */ + public final int st_uid; /*uid_t*/ + + /** Group ID of file. */ + public final int st_gid; /*gid_t*/ + + /** Device ID (if file is character or block special). */ + public final long st_rdev; /*dev_t*/ + + /** + * For regular files, the file size in bytes. + * For symbolic links, the length in bytes of the pathname contained in the symbolic link. + * For a shared memory object, the length in bytes. + * For a typed memory object, the length in bytes. + * For other file types, the use of this field is unspecified. + */ + public final long st_size; /*off_t*/ + + /** Seconds part of time of last access. */ + public final long st_atime; /*time_t*/ + + /** StructTimespec with time of last access. */ + public final StructTimespec st_atim; + + /** Seconds part of time of last data modification. */ + public final long st_mtime; /*time_t*/ + + /** StructTimespec with time of last modification. */ + public final StructTimespec st_mtim; + + /** Seconds part of time of last status change */ + public final long st_ctime; /*time_t*/ + + /** StructTimespec with time of last status change. */ + public final StructTimespec st_ctim; + + /** + * A file system-specific preferred I/O block size for this object. + * For some file system types, this may vary from file to file. + */ + public final long st_blksize; /*blksize_t*/ + + /** Number of blocks allocated for this object. */ + public final long st_blocks; /*blkcnt_t*/ + + /** + * Constructs an instance with the given field values. + */ + public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, + long st_rdev, long st_size, long st_atime, long st_mtime, long st_ctime, + long st_blksize, long st_blocks) { + this(st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid, + st_rdev, st_size, new StructTimespec(st_atime, 0L), new StructTimespec(st_mtime, 0L), + new StructTimespec(st_ctime, 0L), st_blksize, st_blocks); + } + + /** + * Constructs an instance with the given field values. + */ + public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, + long st_rdev, long st_size, StructTimespec st_atim, StructTimespec st_mtim, + StructTimespec st_ctim, long st_blksize, long st_blocks) { + this.st_dev = st_dev; + this.st_ino = st_ino; + this.st_mode = st_mode; + this.st_nlink = st_nlink; + this.st_uid = st_uid; + this.st_gid = st_gid; + this.st_rdev = st_rdev; + this.st_size = st_size; + this.st_atime = st_atim.tv_sec; + this.st_mtime = st_mtim.tv_sec; + this.st_ctime = st_ctim.tv_sec; + this.st_atim = st_atim; + this.st_mtim = st_mtim; + this.st_ctim = st_ctim; + this.st_blksize = st_blksize; + this.st_blocks = st_blocks; + } + + @Override public String toString() { + return Objects.toString(this); + } +} diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java new file mode 100644 index 000000000000..c10678057eeb --- /dev/null +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.system; + +import libcore.util.Objects;; + +/** + * Corresponds to C's {@code struct timespec} from {@code <time.h>}. + */ +public final class StructTimespec implements Comparable<StructTimespec> { + /** Seconds part of time of last data modification. */ + public final long tv_sec; /*time_t*/ + + /** Nanoseconds (values are [0, 999999999]). */ + public final long tv_nsec; + + public StructTimespec(long tv_sec, long tv_nsec) { + this.tv_sec = tv_sec; + this.tv_nsec = tv_nsec; + if (tv_nsec < 0 || tv_nsec > 999_999_999) { + throw new IllegalArgumentException( + "tv_nsec value " + tv_nsec + " is not in [0, 999999999]"); + } + } + + @Override + public int compareTo(StructTimespec other) { + if (tv_sec > other.tv_sec) { + return 1; + } + if (tv_sec < other.tv_sec) { + return -1; + } + if (tv_nsec > other.tv_nsec) { + return 1; + } + if (tv_nsec < other.tv_nsec) { + return -1; + } + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StructTimespec that = (StructTimespec) o; + + if (tv_sec != that.tv_sec) return false; + return tv_nsec == that.tv_nsec; + } + + @Override + public int hashCode() { + int result = (int) (tv_sec ^ (tv_sec >>> 32)); + result = 31 * result + (int) (tv_nsec ^ (tv_nsec >>> 32)); + return result; + } + + @Override + public String toString() { + return Objects.toString(this); + } +} diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java index 65402219ebee..e9b305e5d789 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java +++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java @@ -15,6 +15,9 @@ */ package com.android.ravenwood.common; +import android.system.ErrnoException; +import android.system.StructStat; + import java.io.FileDescriptor; /** @@ -31,19 +34,25 @@ public class RavenwoodRuntimeNative { public static native void applyFreeFunction(long freeFunction, long nativePtr); - public static native long nLseek(int fd, long offset, int whence); + private static native long nLseek(int fd, long offset, int whence) throws ErrnoException; + + private static native int[] nPipe2(int flags) throws ErrnoException; + + private static native int nDup(int oldfd) throws ErrnoException; + + private static native int nFcntlInt(int fd, int cmd, int arg) throws ErrnoException; - public static native int[] nPipe2(int flags); + private static native StructStat nFstat(int fd) throws ErrnoException; - public static native int nDup(int oldfd); + public static native StructStat lstat(String path) throws ErrnoException; - public static native int nFcntlInt(int fd, int cmd, int arg); + public static native StructStat stat(String path) throws ErrnoException; - public static long lseek(FileDescriptor fd, long offset, int whence) { + public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException { return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence); } - public static FileDescriptor[] pipe2(int flags) { + public static FileDescriptor[] pipe2(int flags) throws ErrnoException { var fds = nPipe2(flags); var ret = new FileDescriptor[] { new FileDescriptor(), @@ -55,7 +64,7 @@ public class RavenwoodRuntimeNative { return ret; } - public static FileDescriptor dup(FileDescriptor fd) { + public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException { var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd)); var retFd = new java.io.FileDescriptor(); @@ -63,9 +72,15 @@ public class RavenwoodRuntimeNative { return retFd; } - public static int fcntlInt(FileDescriptor fd, int cmd, int arg) { + public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException { var fdInt = JvmWorkaround.getInstance().getFdInt(fd); return nFcntlInt(fdInt, cmd, arg); } + + public static StructStat fstat(FileDescriptor fd) throws ErrnoException { + var fdInt = JvmWorkaround.getInstance().getFdInt(fd); + + return nFstat(fdInt); + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java new file mode 100644 index 000000000000..3781fcf4e891 --- /dev/null +++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 libcore.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +public final class Objects { + private Objects() {} + + /** + * Returns a string reporting the value of each declared field, via reflection. + * Static and transient fields are automatically skipped. Produces output like + * "SimpleClassName[integer=1234,string="hello",character='c',intArray=[1,2,3]]". + */ + public static String toString(Object o) { + Class<?> c = o.getClass(); + StringBuilder sb = new StringBuilder(); + sb.append(c.getSimpleName()).append('['); + int i = 0; + for (Field f : c.getDeclaredFields()) { + if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) { + continue; + } + f.setAccessible(true); + try { + Object value = f.get(o); + + if (i++ > 0) { + sb.append(','); + } + + sb.append(f.getName()); + sb.append('='); + + if (value.getClass().isArray()) { + if (value.getClass() == boolean[].class) { + sb.append(Arrays.toString((boolean[]) value)); + } else if (value.getClass() == byte[].class) { + sb.append(Arrays.toString((byte[]) value)); + } else if (value.getClass() == char[].class) { + sb.append(Arrays.toString((char[]) value)); + } else if (value.getClass() == double[].class) { + sb.append(Arrays.toString((double[]) value)); + } else if (value.getClass() == float[].class) { + sb.append(Arrays.toString((float[]) value)); + } else if (value.getClass() == int[].class) { + sb.append(Arrays.toString((int[]) value)); + } else if (value.getClass() == long[].class) { + sb.append(Arrays.toString((long[]) value)); + } else if (value.getClass() == short[].class) { + sb.append(Arrays.toString((short[]) value)); + } else { + sb.append(Arrays.toString((Object[]) value)); + } + } else if (value.getClass() == Character.class) { + sb.append('\'').append(value).append('\''); + } else if (value.getClass() == String.class) { + sb.append('"').append(value).append('"'); + } else { + sb.append(value); + } + } catch (IllegalAccessException unexpected) { + throw new AssertionError(unexpected); + } + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp index 34cf9f915677..e0a3e1c9edf6 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -19,6 +19,9 @@ #include <string.h> #include <unistd.h> #include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> +#include <nativehelper/ScopedUtfChars.h> + #include "jni.h" #include "utils/Log.h" #include "utils/misc.h" @@ -41,6 +44,75 @@ static rc_t throwIfMinusOne(JNIEnv* env, const char* name, rc_t rc) { return rc; } +// ---- Helper functions --- + +static jclass g_StructStat; +static jclass g_StructTimespecClass; + +static jclass findClass(JNIEnv* env, const char* name) { + ScopedLocalRef<jclass> localClass(env, env->FindClass(name)); + jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get())); + if (result == NULL) { + ALOGE("failed to find class '%s'", name); + abort(); + } + return result; +} + +static jobject makeStructTimespec(JNIEnv* env, const struct timespec& ts) { + static jmethodID ctor = env->GetMethodID(g_StructTimespecClass, "<init>", + "(JJ)V"); + if (ctor == NULL) { + return NULL; + } + return env->NewObject(g_StructTimespecClass, ctor, + static_cast<jlong>(ts.tv_sec), static_cast<jlong>(ts.tv_nsec)); +} + +static jobject makeStructStat(JNIEnv* env, const struct stat64& sb) { + static jmethodID ctor = env->GetMethodID(g_StructStat, "<init>", + "(JJIJIIJJLandroid/system/StructTimespec;Landroid/system/StructTimespec;Landroid/system/StructTimespec;JJ)V"); + if (ctor == NULL) { + return NULL; + } + + jobject atim_timespec = makeStructTimespec(env, sb.st_atim); + if (atim_timespec == NULL) { + return NULL; + } + jobject mtim_timespec = makeStructTimespec(env, sb.st_mtim); + if (mtim_timespec == NULL) { + return NULL; + } + jobject ctim_timespec = makeStructTimespec(env, sb.st_ctim); + if (ctim_timespec == NULL) { + return NULL; + } + + return env->NewObject(g_StructStat, ctor, + static_cast<jlong>(sb.st_dev), static_cast<jlong>(sb.st_ino), + static_cast<jint>(sb.st_mode), static_cast<jlong>(sb.st_nlink), + static_cast<jint>(sb.st_uid), static_cast<jint>(sb.st_gid), + static_cast<jlong>(sb.st_rdev), static_cast<jlong>(sb.st_size), + atim_timespec, mtim_timespec, ctim_timespec, + static_cast<jlong>(sb.st_blksize), static_cast<jlong>(sb.st_blocks)); +} + +static jobject doStat(JNIEnv* env, jstring javaPath, bool isLstat) { + ScopedUtfChars path(env, javaPath); + if (path.c_str() == NULL) { + return NULL; + } + struct stat64 sb; + int rc = isLstat ? TEMP_FAILURE_RETRY(lstat64(path.c_str(), &sb)) + : TEMP_FAILURE_RETRY(stat64(path.c_str(), &sb)); + if (rc == -1) { + throwErrnoException(env, isLstat ? "lstat" : "stat"); + return NULL; + } + return makeStructStat(env, sb); +} + // ---- JNI methods ---- typedef void (*FreeFunction)(void*); @@ -77,6 +149,24 @@ static jlong nDup(JNIEnv* env, jclass, jint fd) { return throwIfMinusOne(env, "fcntl", TEMP_FAILURE_RETRY(fcntl(fd, F_DUPFD_CLOEXEC, 0))); } +static jobject nFstat(JNIEnv* env, jobject, jint fd) { + struct stat64 sb; + int rc = TEMP_FAILURE_RETRY(fstat64(fd, &sb)); + if (rc == -1) { + throwErrnoException(env, "fstat"); + return NULL; + } + return makeStructStat(env, sb); +} + +static jobject Linux_lstat(JNIEnv* env, jobject, jstring javaPath) { + return doStat(env, javaPath, true); +} + +static jobject Linux_stat(JNIEnv* env, jobject, jstring javaPath) { + return doStat(env, javaPath, false); +} + // ---- Registration ---- static const JNINativeMethod sMethods[] = @@ -86,6 +176,9 @@ static const JNINativeMethod sMethods[] = { "nLseek", "(IJI)J", (void*)nLseek }, { "nPipe2", "(I)[I", (void*)nPipe2 }, { "nDup", "(I)I", (void*)nDup }, + { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat }, + { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat }, + { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat }, }; extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) @@ -101,6 +194,9 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) ALOGI("%s: JNI_OnLoad", __FILE__); + g_StructStat = findClass(env, "android/system/StructStat"); + g_StructTimespecClass = findClass(env, "android/system/StructTimespec"); + jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative", sMethods, NELEM(sMethods)); if (res < 0) { diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java index b5038e68516d..05275b29e48b 100644 --- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java +++ b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java @@ -15,15 +15,26 @@ */ package com.android.ravenwood.runtimetest; +import static android.system.OsConstants.S_ISBLK; +import static android.system.OsConstants.S_ISCHR; +import static android.system.OsConstants.S_ISDIR; +import static android.system.OsConstants.S_ISFIFO; +import static android.system.OsConstants.S_ISLNK; +import static android.system.OsConstants.S_ISREG; +import static android.system.OsConstants.S_ISSOCK; + import static org.junit.Assert.assertEquals; +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; + import android.system.Os; import android.system.OsConstants; +import android.system.StructStat; +import android.system.StructTimespec; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.ravenwood.common.JvmWorkaround; -import com.android.ravenwood.common.RavenwoodRuntimeNative; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,8 +43,15 @@ import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) public class OsTest { @@ -41,7 +59,7 @@ public class OsTest { void accept(T var1) throws Exception; } - private void withTestFile(ConsumerWithThrow<FileDescriptor> consumer) throws Exception { + private void withTestFileFD(ConsumerWithThrow<FileDescriptor> consumer) throws Exception { File file = File.createTempFile("osTest", "bin"); try (var raf = new RandomAccessFile(file, "rw")) { var fd = raf.getFD(); @@ -57,9 +75,20 @@ public class OsTest { } } + private void withTestFile(ConsumerWithThrow<Path> consumer) throws Exception { + var path = Files.createTempFile("osTest", "bin"); + try (var os = Files.newOutputStream(path)) { + os.write(1); + os.write(2); + os.write(3); + os.write(4); + } + consumer.accept(path); + } + @Test public void testLseek() throws Exception { - withTestFile((fd) -> { + withTestFileFD((fd) -> { assertEquals(4, Os.lseek(fd, 4, OsConstants.SEEK_SET)); assertEquals(4, Os.lseek(fd, 0, OsConstants.SEEK_CUR)); assertEquals(6, Os.lseek(fd, 2, OsConstants.SEEK_CUR)); @@ -68,7 +97,7 @@ public class OsTest { @Test public void testDup() throws Exception { - withTestFile((fd) -> { + withTestFileFD((fd) -> { var dup = Os.dup(fd); checkAreDup(fd, dup); @@ -85,7 +114,7 @@ public class OsTest { @Test public void testFcntlInt() throws Exception { - withTestFile((fd) -> { + withTestFileFD((fd) -> { var dupInt = Os.fcntlInt(fd, 0, 0); var dup = new FileDescriptor(); @@ -95,16 +124,90 @@ public class OsTest { }); } - private static void write(FileDescriptor fd, int oneByte) throws IOException { + @Test + public void testStat() throws Exception { + withTestFile(path -> { + var attr = Files.readAttributes(path, PosixFileAttributes.class); + var stat = Os.stat(path.toAbsolutePath().toString()); + assertAttributesEqual(attr, stat); + }); + } + + @Test + public void testLstat() throws Exception { + withTestFile(path -> { + // Create a symbolic link + var lnk = Files.createTempFile("osTest", "lnk"); + Files.delete(lnk); + Files.createSymbolicLink(lnk, path); + + // Test lstat + var attr = Files.readAttributes(lnk, PosixFileAttributes.class, NOFOLLOW_LINKS); + var stat = Os.lstat(lnk.toAbsolutePath().toString()); + assertAttributesEqual(attr, stat); + + // Test stat + var followAttr = Files.readAttributes(lnk, PosixFileAttributes.class); + var followStat = Os.stat(lnk.toAbsolutePath().toString()); + assertAttributesEqual(followAttr, followStat); + }); + } + + @Test + public void testFstat() throws Exception { + withTestFile(path -> { + var attr = Files.readAttributes(path, PosixFileAttributes.class); + try (var raf = new RandomAccessFile(path.toFile(), "r")) { + var fd = raf.getFD(); + var stat = Os.fstat(fd); + assertAttributesEqual(attr, stat); + } + }); + } + + // Verify StructStat values from libcore against native JVM PosixFileAttributes + private static void assertAttributesEqual(PosixFileAttributes attr, StructStat stat) { + assertEquals(attr.lastModifiedTime(), convertTimespecToFileTime(stat.st_mtim)); + assertEquals(attr.size(), stat.st_size); + assertEquals(attr.isDirectory(), S_ISDIR(stat.st_mode)); + assertEquals(attr.isRegularFile(), S_ISREG(stat.st_mode)); + assertEquals(attr.isSymbolicLink(), S_ISLNK(stat.st_mode)); + assertEquals(attr.isOther(), S_ISCHR(stat.st_mode) + || S_ISBLK(stat.st_mode) || S_ISFIFO(stat.st_mode) || S_ISSOCK(stat.st_mode)); + assertEquals(attr.permissions(), convertModeToPosixPerms(stat.st_mode)); + + } + + private static FileTime convertTimespecToFileTime(StructTimespec ts) { + var nanos = TimeUnit.SECONDS.toNanos(ts.tv_sec); + nanos += ts.tv_nsec; + return FileTime.from(nanos, TimeUnit.NANOSECONDS); + } + + private static Set<PosixFilePermission> convertModeToPosixPerms(int mode) { + var set = new HashSet<PosixFilePermission>(); + if ((mode & OsConstants.S_IRUSR) != 0) set.add(PosixFilePermission.OWNER_READ); + if ((mode & OsConstants.S_IWUSR) != 0) set.add(PosixFilePermission.OWNER_WRITE); + if ((mode & OsConstants.S_IXUSR) != 0) set.add(PosixFilePermission.OWNER_EXECUTE); + if ((mode & OsConstants.S_IRGRP) != 0) set.add(PosixFilePermission.GROUP_READ); + if ((mode & OsConstants.S_IWGRP) != 0) set.add(PosixFilePermission.GROUP_WRITE); + if ((mode & OsConstants.S_IXGRP) != 0) set.add(PosixFilePermission.GROUP_EXECUTE); + if ((mode & OsConstants.S_IROTH) != 0) set.add(PosixFilePermission.OTHERS_READ); + if ((mode & OsConstants.S_IWOTH) != 0) set.add(PosixFilePermission.OTHERS_WRITE); + if ((mode & OsConstants.S_IXOTH) != 0) set.add(PosixFilePermission.OTHERS_EXECUTE); + return set; + } + + private static void write(FileDescriptor fd, int oneByte) throws Exception { // Create a dup to avoid closing the FD. - try (var dup = new FileOutputStream(RavenwoodRuntimeNative.dup(fd))) { + try (var dup = new FileOutputStream(Os.dup(fd))) { dup.write(oneByte); } } - private static int read(FileDescriptor fd) throws IOException { + private static int read(FileDescriptor fd) throws Exception { // Create a dup to avoid closing the FD. - try (var dup = new FileInputStream(RavenwoodRuntimeNative.dup(fd))) { + try (var dup = new FileInputStream(Os.dup(fd))) { return dup.read(); } } diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp index 7a99b605c4fb..311addb90298 100644 --- a/services/accessibility/Android.bp +++ b/services/accessibility/Android.bp @@ -29,10 +29,12 @@ java_library_static { "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc", ], libs: [ + "aatf", "services.core", "androidx.annotation_annotation", ], static_libs: [ + "a11ychecker-protos-java-proto-lite", "com_android_server_accessibility_flags_lib", "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib", @@ -68,3 +70,14 @@ java_aconfig_library { name: "com_android_server_accessibility_flags_lib", aconfig_declarations: "com_android_server_accessibility_flags", } + +java_library_static { + name: "a11ychecker-protos-java-proto-lite", + proto: { + type: "lite", + canonical_path_from_root: false, + }, + srcs: [ + "java/**/a11ychecker/proto/*.proto", + ], +} diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java new file mode 100644 index 000000000000..55af9a0cfd24 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java @@ -0,0 +1,218 @@ +/* + * Copyright 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.server.accessibility.a11ychecker; + + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Slog; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass; +import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported; +import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType; + +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.TextSizeCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck; + +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Util class to process a11y checker results for logging. + * + * @hide + */ +public class AccessibilityCheckerUtils { + + private static final String LOG_TAG = "AccessibilityCheckerUtils"; + @VisibleForTesting + // LINT.IfChange + static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass> + CHECK_CLASS_TO_ENUM_MAP = + Map.ofEntries( + classMapEntry(ClassNameCheck.class, AccessibilityCheckClass.CLASS_NAME_CHECK), + classMapEntry(ClickableSpanCheck.class, + AccessibilityCheckClass.CLICKABLE_SPAN_CHECK), + classMapEntry(DuplicateClickableBoundsCheck.class, + AccessibilityCheckClass.DUPLICATE_CLICKABLE_BOUNDS_CHECK), + classMapEntry(DuplicateSpeakableTextCheck.class, + AccessibilityCheckClass.DUPLICATE_SPEAKABLE_TEXT_CHECK), + classMapEntry(EditableContentDescCheck.class, + AccessibilityCheckClass.EDITABLE_CONTENT_DESC_CHECK), + classMapEntry(ImageContrastCheck.class, + AccessibilityCheckClass.IMAGE_CONTRAST_CHECK), + classMapEntry(LinkPurposeUnclearCheck.class, + AccessibilityCheckClass.LINK_PURPOSE_UNCLEAR_CHECK), + classMapEntry(RedundantDescriptionCheck.class, + AccessibilityCheckClass.REDUNDANT_DESCRIPTION_CHECK), + classMapEntry(SpeakableTextPresentCheck.class, + AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK), + classMapEntry(TextContrastCheck.class, + AccessibilityCheckClass.TEXT_CONTRAST_CHECK), + classMapEntry(TextSizeCheck.class, AccessibilityCheckClass.TEXT_SIZE_CHECK), + classMapEntry(TouchTargetSizeCheck.class, + AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK), + classMapEntry(TraversalOrderCheck.class, + AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK)); + // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto) + + static Set<AccessibilityCheckResultReported> processResults( + Context context, + AccessibilityNodeInfo nodeInfo, + List<AccessibilityHierarchyCheckResult> checkResults, + @Nullable AccessibilityEvent accessibilityEvent, + ComponentName a11yServiceComponentName) { + return processResults(nodeInfo, checkResults, accessibilityEvent, + context.getPackageManager(), a11yServiceComponentName); + } + + @VisibleForTesting + static Set<AccessibilityCheckResultReported> processResults( + AccessibilityNodeInfo nodeInfo, + List<AccessibilityHierarchyCheckResult> checkResults, + @Nullable AccessibilityEvent accessibilityEvent, + PackageManager packageManager, + ComponentName a11yServiceComponentName) { + String appPackageName = nodeInfo.getPackageName().toString(); + AccessibilityCheckResultReported.Builder builder; + try { + builder = AccessibilityCheckResultReported.newBuilder() + .setPackageName(appPackageName) + .setAppVersionCode(getAppVersionCode(packageManager, appPackageName)) + .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo)) + .setActivityName(getActivityName(packageManager, accessibilityEvent)) + .setWindowTitle(getWindowTitle(nodeInfo)) + .setSourceComponentName(a11yServiceComponentName.flattenToString()) + .setSourceVersionCode( + getAppVersionCode(packageManager, + a11yServiceComponentName.getPackageName())); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(LOG_TAG, "Unknown package name", e); + return Set.of(); + } + + return checkResults.stream() + .filter(checkResult -> checkResult.getType() + == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR + || checkResult.getType() + == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING) + .map(checkResult -> builder.setResultCheckClass( + getCheckClass(checkResult)).setResultType( + getCheckResultType(checkResult)).setResultId( + checkResult.getResultId()).build()) + .collect(Collectors.toUnmodifiableSet()); + } + + private static long getAppVersionCode(PackageManager packageManager, String packageName) throws + PackageManager.NameNotFoundException { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + return packageInfo.getLongVersionCode(); + } + + /** + * Returns the simple class name of the Activity providing the cache update, if available, + * or an empty String if not. + */ + @VisibleForTesting + static String getActivityName( + PackageManager packageManager, @Nullable AccessibilityEvent accessibilityEvent) { + if (accessibilityEvent == null) { + return ""; + } + CharSequence activityName = accessibilityEvent.getClassName(); + if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && accessibilityEvent.getPackageName() != null + && activityName != null) { + try { + // Check class is for a valid Activity. + packageManager + .getActivityInfo( + new ComponentName(accessibilityEvent.getPackageName().toString(), + activityName.toString()), 0); + int qualifierEnd = activityName.toString().lastIndexOf('.'); + return activityName.toString().substring(qualifierEnd + 1); + } catch (PackageManager.NameNotFoundException e) { + // No need to spam the logs. This is very frequent when the class doesn't match + // an activity. + } + } + return ""; + } + + /** + * Returns the title of the window containing the a11y node. + */ + private static String getWindowTitle(AccessibilityNodeInfo nodeInfo) { + if (nodeInfo.getWindow() == null) { + return ""; + } + CharSequence windowTitle = nodeInfo.getWindow().getTitle(); + return windowTitle == null ? "" : windowTitle.toString(); + } + + /** + * Maps the {@link AccessibilityHierarchyCheck} class that produced the given result, with the + * corresponding {@link AccessibilityCheckClass} enum. This enumeration is to avoid relying on + * String class names in the logging, which can be proguarded. It also reduces the logging size. + */ + private static AccessibilityCheckClass getCheckClass( + AccessibilityHierarchyCheckResult checkResult) { + if (CHECK_CLASS_TO_ENUM_MAP.containsKey(checkResult.getSourceCheckClass())) { + return CHECK_CLASS_TO_ENUM_MAP.get(checkResult.getSourceCheckClass()); + } + return AccessibilityCheckClass.UNKNOWN_CHECK; + } + + private static AccessibilityCheckResultType getCheckResultType( + AccessibilityHierarchyCheckResult checkResult) { + return switch (checkResult.getType()) { + case ERROR -> AccessibilityCheckResultType.ERROR; + case WARNING -> AccessibilityCheckResultType.WARNING; + default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE; + }; + } + + private static Map.Entry<Class<? extends AccessibilityHierarchyCheck>, + AccessibilityCheckClass> classMapEntry( + Class<? extends AccessibilityHierarchyCheck> checkClass, + AccessibilityCheckClass checkClassEnum) { + return new AbstractMap.SimpleImmutableEntry<>(checkClass, checkClassEnum); + } +} diff --git a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java index 2996ddef6f64..bbfb217d925e 100644 --- a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.view.accessibility.a11ychecker; +package com.android.server.accessibility.a11ychecker; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/core/java/android/view/accessibility/a11ychecker/OWNERS b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS index d1e7986cc209..d1e7986cc209 100644 --- a/core/java/android/view/accessibility/a11ychecker/OWNERS +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto new file mode 100644 index 000000000000..8beed4afa9cb --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto @@ -0,0 +1,73 @@ +/* + * 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. + */ +syntax = "proto2"; +package android.accessibility; + +option java_package = "com.android.server.accessibility.a11ychecker"; +option java_outer_classname = "A11yCheckerProto"; + +// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted. +/** Logs the result of an AccessibilityCheck. */ +message AccessibilityCheckResultReported { + // Package name of the app containing the checked View. + optional string package_name = 1; + // Version code of the app containing the checked View. + optional int64 app_version_code = 2; + // The path of the View starting from the root element in the window. Each element is + // represented by the View's resource id, when available, or the View's class name. + optional string ui_element_path = 3; + // Class name of the activity containing the checked View. + optional string activity_name = 4; + // Title of the window containing the checked View. + optional string window_title = 5; + // The flattened component name of the app running the AccessibilityService which provided the a11y node. + optional string source_component_name = 6; + // Version code of the app running the AccessibilityService that provided the a11y node. + optional int64 source_version_code = 7; + // Class Name of the AccessibilityCheck that produced the result. + optional AccessibilityCheckClass result_check_class = 8; + // Result type of the AccessibilityCheckResult. + optional AccessibilityCheckResultType result_type = 9; + // Result ID of the AccessibilityCheckResult. + optional int32 result_id = 10; +} + +/** The AccessibilityCheck class. */ +// LINT.IfChange +enum AccessibilityCheckClass { + UNKNOWN_CHECK = 0; + CLASS_NAME_CHECK = 1; + CLICKABLE_SPAN_CHECK = 2; + DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3; + DUPLICATE_SPEAKABLE_TEXT_CHECK = 4; + EDITABLE_CONTENT_DESC_CHECK = 5; + IMAGE_CONTRAST_CHECK = 6; + LINK_PURPOSE_UNCLEAR_CHECK = 7; + REDUNDANT_DESCRIPTION_CHECK = 8; + SPEAKABLE_TEXT_PRESENT_CHECK = 9; + TEXT_CONTRAST_CHECK = 10; + TEXT_SIZE_CHECK = 11; + TOUCH_TARGET_SIZE_CHECK = 12; + TRAVERSAL_ORDER_CHECK = 13; +} +// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java) + +/** The type of AccessibilityCheckResult */ +enum AccessibilityCheckResultType { + UNKNOWN_RESULT_TYPE = 0; + ERROR = 1; + WARNING = 2; +} diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index d7da2f0052d3..026c69c2378c 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -846,7 +846,6 @@ public final class PresentationStatsEventLogger { mCallingAppUid, event.mIsCredentialRequest, event.mWebviewRequestedCredential, - event.mFilteredFillabaleViewCount, event.mViewFillableTotalCount, event.mViewFillFailureCount, event.mFocusedId, @@ -859,7 +858,8 @@ public final class PresentationStatsEventLogger { event.mSuggestionPresentedLastTimestampMs, event.mFocusedVirtualAutofillId, event.mFieldFirstLength, - event.mFieldLastLength); + event.mFieldLastLength, + event.mFilteredFillabaleViewCount); mEventInternal = Optional.empty(); } diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/services/core/java/com/android/server/ExplicitHealthCheckController.java index 3d610d3747c9..6a6aea49f6f2 100644 --- a/services/core/java/com/android/server/ExplicitHealthCheckController.java +++ b/services/core/java/com/android/server/ExplicitHealthCheckController.java @@ -15,6 +15,7 @@ */ package com.android.server; +import static android.crashrecovery.flags.Flags.refactorCrashrecovery; import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; @@ -41,7 +42,6 @@ import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.Preconditions; import java.util.Collection; import java.util.Collections; @@ -363,22 +363,34 @@ class ExplicitHealthCheckController { @GuardedBy("mLock") @Nullable private ServiceInfo getServiceInfoLocked() { - final String packageName = - mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); - if (packageName == null) { - Slog.w(TAG, "no external services package!"); - return null; - } + if (refactorCrashrecovery()) { + final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA + | PackageManager.MATCH_SYSTEM_ONLY); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "No valid components found."); + return null; + } + return resolveInfo.serviceInfo; + } else { + final String packageName = + mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); + if (packageName == null) { + Slog.w(TAG, "no external services package!"); + return null; + } - final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); - intent.setPackage(packageName); - final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, - PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); - if (resolveInfo == null || resolveInfo.serviceInfo == null) { - Slog.w(TAG, "No valid components found."); - return null; + final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); + intent.setPackage(packageName); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "No valid components found."); + return null; + } + return resolveInfo.serviceInfo; } - return resolveInfo.serviceInfo; } @GuardedBy("mLock") diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 458749d93e0a..2d913311dae9 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -185,6 +185,12 @@ public class AccountManagerService final Context mContext; + private static final int[] INTERESTING_APP_OPS = new int[] { + AppOpsManager.OP_GET_ACCOUNTS, + AppOpsManager.OP_READ_CONTACTS, + AppOpsManager.OP_WRITE_CONTACTS, + }; + private final PackageManager mPackageManager; private final AppOpsManager mAppOpsManager; private UserManager mUserManager; @@ -388,74 +394,47 @@ public class AccountManagerService }.register(mContext, mHandler.getLooper(), UserHandle.ALL, true); // Cancel account request notification if an app op was preventing the account access - mAppOpsManager.startWatchingMode(AppOpsManager.OP_GET_ACCOUNTS, null, - new AppOpsManager.OnOpChangedInternalListener() { - @Override - public void onOpChanged(int op, String packageName) { - try { - final int userId = ActivityManager.getCurrentUser(); - final int uid = mPackageManager.getPackageUidAsUser(packageName, userId); - final int mode = mAppOpsManager.checkOpNoThrow( - AppOpsManager.OP_GET_ACCOUNTS, uid, packageName); - if (mode == AppOpsManager.MODE_ALLOWED) { - final long identity = Binder.clearCallingIdentity(); - try { - UserAccounts accounts = getUserAccounts(userId); - cancelAccountAccessRequestNotificationIfNeeded( - packageName, uid, true, accounts); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - } catch (NameNotFoundException e) { - /* ignore */ - } catch (SQLiteCantOpenDatabaseException e) { - Log.w(TAG, "Can't read accounts database", e); - return; - } - } - }); + for (int i = 0; i < INTERESTING_APP_OPS.length; ++i) { + mAppOpsManager.startWatchingMode(INTERESTING_APP_OPS[i], null, + new OnInterestingAppOpChangedListener()); + } - // Cancel account request notification if a permission was preventing the account access - mPackageManager.addOnPermissionsChangeListener( - (int uid) -> { - // Permission changes cause requires updating accounts cache. + // Clear the accounts cache on permission changes. + // The specific permissions we care about are backed by AppOps, so just + // let the change events on those handle clearing any notifications. + mPackageManager.addOnPermissionsChangeListener((int uid) -> { AccountManager.invalidateLocalAccountsDataCaches(); + }); + } - Account[] accounts = null; - String[] packageNames = mPackageManager.getPackagesForUid(uid); - if (packageNames != null) { - final int userId = UserHandle.getUserId(uid); - final long identity = Binder.clearCallingIdentity(); - try { - for (String packageName : packageNames) { - // if app asked for permission we need to cancel notification even - // for O+ applications. - if (mPackageManager.checkPermission( - Manifest.permission.GET_ACCOUNTS, - packageName) != PackageManager.PERMISSION_GRANTED) { - continue; - } + private class OnInterestingAppOpChangedListener implements AppOpsManager.OnOpChangedListener { + @Override + public void onOpChanged(String op, String packageName) { + final int userId = ActivityManager.getCurrentUser(); + final int packageUid; + try { + packageUid = mPackageManager.getPackageUidAsUser(packageName, userId); + } catch (NameNotFoundException e) { + /* ignore */ + return; + } - if (accounts == null) { - accounts = getAccountsOrEmptyArray(null, userId, "android"); - if (ArrayUtils.isEmpty(accounts)) { - return; - } - } - UserAccounts userAccounts = getUserAccounts(UserHandle.getUserId(uid)); - for (Account account : accounts) { - cancelAccountAccessRequestNotificationIfNeeded( - account, uid, packageName, true, userAccounts); - } - } - } finally { - Binder.restoreCallingIdentity(identity); - } + final int mode = mAppOpsManager.checkOpNoThrow(op, packageUid, packageName); + if (mode != AppOpsManager.MODE_ALLOWED) { + return; } - }); - } + final long identity = Binder.clearCallingIdentity(); + try { + cancelAccountAccessRequestNotificationIfNeeded( + packageName, packageUid, true, getUserAccounts(userId)); + } catch (SQLiteCantOpenDatabaseException e) { + Log.w(TAG, "Can't read accounts database", e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } boolean getBindInstantServiceAllowed(int userId) { return mAuthenticatorCache.getBindInstantServiceAllowed(userId); diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index 12ec24819c78..222c5a83a551 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -16,7 +16,6 @@ package com.android.server.display; -import android.hardware.display.BrightnessInfo; import android.text.TextUtils; import com.android.server.display.brightness.BrightnessEvent; @@ -51,8 +50,6 @@ public final class DisplayBrightnessState { private final boolean mIsUserInitiatedChange; - private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason; - private DisplayBrightnessState(Builder builder) { mBrightness = builder.getBrightness(); mHdrBrightness = builder.getHdrBrightness(); @@ -67,7 +64,6 @@ public final class DisplayBrightnessState { mBrightnessEvent = builder.getBrightnessEvent(); mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag(); mIsUserInitiatedChange = builder.isUserInitiatedChange(); - mBrightnessMaxReason = builder.getBrightnessMaxReason(); } /** @@ -163,13 +159,6 @@ public final class DisplayBrightnessState { return mIsUserInitiatedChange; } - /** - * Gets reason for max brightness restriction - */ - public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() { - return mBrightnessMaxReason; - } - @Override public String toString() { StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:"); @@ -191,8 +180,6 @@ public final class DisplayBrightnessState { .append(Objects.toString(mBrightnessEvent, "null")); stringBuilder.append("\n mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag); stringBuilder.append("\n mIsUserInitiatedChange:").append(mIsUserInitiatedChange); - stringBuilder.append("\n mBrightnessMaxReason:") - .append(BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason)); return stringBuilder.toString(); } @@ -225,8 +212,7 @@ public final class DisplayBrightnessState { == otherState.shouldUpdateScreenBrightnessSetting() && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent()) && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag() - && mIsUserInitiatedChange == otherState.isUserInitiatedChange() - && mBrightnessMaxReason == otherState.getBrightnessMaxReason(); + && mIsUserInitiatedChange == otherState.isUserInitiatedChange(); } @Override @@ -235,7 +221,7 @@ public final class DisplayBrightnessState { mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness, mCustomAnimationRate, mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag, - mIsUserInitiatedChange, mBrightnessMaxReason); + mIsUserInitiatedChange); } /** @@ -259,11 +245,12 @@ public final class DisplayBrightnessState { private float mMinBrightness; private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET; private boolean mShouldUpdateScreenBrightnessSetting; + private BrightnessEvent mBrightnessEvent; - private int mBrightnessAdjustmentFlag = 0; + + public int mBrightnessAdjustmentFlag = 0; + private boolean mIsUserInitiatedChange; - private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason = - BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; /** * Create a builder starting with the values from the specified {@link @@ -287,7 +274,6 @@ public final class DisplayBrightnessState { builder.setBrightnessEvent(state.getBrightnessEvent()); builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag()); builder.setIsUserInitiatedChange(state.isUserInitiatedChange()); - builder.setBrightnessMaxReason(state.getBrightnessMaxReason()); return builder; } @@ -510,21 +496,5 @@ public final class DisplayBrightnessState { mIsUserInitiatedChange = isUserInitiatedChange; return this; } - - /** - * Gets reason for max brightness restriction - */ - public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() { - return mBrightnessMaxReason; - } - - /** - * Sets reason for max brightness restriction - */ - public Builder setBrightnessMaxReason( - @BrightnessInfo.BrightnessMaxReason int brightnessMaxReason) { - mBrightnessMaxReason = brightnessMaxReason; - return this; - } } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index f5231ae0abe6..7a055d1d7e5c 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -619,6 +619,15 @@ import javax.xml.datatype.DatatypeConfigurationException; * </idleScreenRefreshRateTimeout> * <supportsVrr>true</supportsVrr> * + * <dozeBrightnessSensorValueToBrightness> + * <item>-1</item> <!-- 0: OFF --> + * <item>0.003937008</item> <!-- 1: NIGHT --> + * <item>0.015748031</item> <!-- 2: LOW --> + * <item>0.102362205</item> <!-- 3: HIGH --> + * <item>0.106299213</item> <!-- 4: SUN --> + * </dozeBrightnessSensorValueToBrightness> + * <defaultDozeBrightness>0.235</defaultDozeBrightness> + * * </displayConfiguration> * } * </pre> @@ -638,6 +647,10 @@ public class DisplayDeviceConfig { public static final int DEFAULT_LOW_REFRESH_RATE = 60; + // Float.NaN (used as invalid for brightness) cannot be stored in config.xml + // so -2 is used instead + public static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f; + @VisibleForTesting static final float BRIGHTNESS_DEFAULT = 0.5f; private static final String ETC_DIR = "etc"; @@ -656,10 +669,6 @@ public class DisplayDeviceConfig { private static final int INTERPOLATION_DEFAULT = 0; private static final int INTERPOLATION_LINEAR = 1; - // Float.NaN (used as invalid for brightness) cannot be stored in config.xml - // so -2 is used instead - private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f; - // Length of the ambient light horizon used to calculate the long term estimate of ambient // light. private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000; @@ -670,6 +679,11 @@ public class DisplayDeviceConfig { // Invalid value of AutoBrightness brightening and darkening light debounce private static final int INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE = -1; + @VisibleForTesting + static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f; + + private static final int KEEP_CURRENT_BRIGHTNESS = -1; + private final Context mContext; // The details of the ambient light sensor associated with this display. @@ -877,6 +891,10 @@ public class DisplayDeviceConfig { private boolean mVrrSupportEnabled; + @Nullable + private float[] mDozeBrightnessSensorValueToBrightness; + private float mDefaultDozeBrightness; + private final DisplayManagerFlags mFlags; @VisibleForTesting @@ -1592,6 +1610,24 @@ public class DisplayDeviceConfig { return mVrrSupportEnabled; } + /** + * While the device is dozing, a designated light sensor is used to determine the brightness. + * @return The mapping between doze brightness sensor values and brightness values. The value + * -1 means that the current brightness should be kept. + */ + @Nullable + public float[] getDozeBrightnessSensorValueToBrightness() { + return mDozeBrightnessSensorValueToBrightness; + } + + /** + * @return The default doze brightness to use while no other doze brightness is available. Can + * be {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} if undefined. + */ + public float getDefaultDozeBrightness() { + return mDefaultDozeBrightness; + } + @Override public String toString() { return "DisplayDeviceConfig{" @@ -1689,6 +1725,9 @@ public class DisplayDeviceConfig { ? mEvenDimmerBrightnessData.toString() : "null") + "\n" + "mVrrSupported= " + mVrrSupportEnabled + "\n" + + "mDozeBrightnessSensorValueToBrightness= " + + Arrays.toString(mDozeBrightnessSensorValueToBrightness) + "\n" + + "mDefaultDozeBrightness= " + mDefaultDozeBrightness + "\n" + "}"; } @@ -1783,6 +1822,7 @@ public class DisplayDeviceConfig { loadBrightnessCapForWearBedtimeMode(config); loadIdleScreenRefreshRateTimeoutConfigs(config); mVrrSupportEnabled = config.getSupportsVrr(); + loadDozeBrightness(config); } else { Slog.w(TAG, "DisplayDeviceConfig file is null"); } @@ -1811,6 +1851,7 @@ public class DisplayDeviceConfig { loadRefreshRateSetting(null); loadBrightnessCapForWearBedtimeModeFromConfigXml(); loadIdleScreenRefreshRateTimeoutConfigs(null); + loadDozeBrightness(null); mLoadedFrom = "<config.xml>"; } @@ -2745,6 +2786,37 @@ public class DisplayDeviceConfig { } } + private void loadDozeBrightness(DisplayConfiguration config) { + if (mFlags.isDozeBrightnessFloatEnabled() && config != null + && config.getDozeBrightnessSensorValueToBrightness() != null) { + List<BigDecimal> values = config.getDozeBrightnessSensorValueToBrightness().getItem(); + mDozeBrightnessSensorValueToBrightness = new float[values.size()]; + for (int i = 0; i < values.size(); i++) { + float backlight = values.get(i).floatValue(); + if (backlight != KEEP_CURRENT_BRIGHTNESS) { + mDozeBrightnessSensorValueToBrightness[i] = + getBrightnessFromBacklight(backlight); + } else { + mDozeBrightnessSensorValueToBrightness[i] = KEEP_CURRENT_BRIGHTNESS; + } + } + } + + if (mFlags.isDozeBrightnessFloatEnabled() && config != null + && config.getDefaultDozeBrightness() != null) { + float backlight = config.getDefaultDozeBrightness().floatValue(); + mDefaultDozeBrightness = getBrightnessFromBacklight(backlight); + } else { + mDefaultDozeBrightness = mContext.getResources().getFloat( + com.android.internal.R.dimen.config_screenBrightnessDozeFloat); + if (mDefaultDozeBrightness == INVALID_BRIGHTNESS_IN_CONFIG) { + mDefaultDozeBrightness = BrightnessSynchronizer.brightnessIntToFloat( + mContext.getResources().getInteger( + com.android.internal.R.integer.config_screenBrightnessDoze)); + } + } + } + private void validateIdleScreenRefreshRateTimeoutConfig( IdleScreenRefreshRateTimeout idleScreenRefreshRateTimeoutConfig) { IdleScreenRefreshRateTimeoutLuxThresholds idleScreenRefreshRateTimeoutLuxThresholds = diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 2f3584cf7cef..b3a6c1c1e20a 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -4726,6 +4726,32 @@ public final class DisplayManagerService extends SystemService { DisplayManagerService.this.mDisplayModeDirector.requestDisplayModes( token, displayId, modeIds); } + + @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @Override // Binder call + public float[] getDozeBrightnessSensorValueToBrightness(int displayId) { + getDozeBrightnessSensorValueToBrightness_enforcePermission(); + DisplayDeviceConfig ddc = + mDisplayDeviceConfigProvider.getDisplayDeviceConfig(displayId); + if (ddc == null) { + throw new IllegalArgumentException( + "Display ID does not have a config: " + displayId); + } + return ddc.getDozeBrightnessSensorValueToBrightness(); + } + + @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS) + @Override // Binder call + public float getDefaultDozeBrightness(int displayId) { + getDefaultDozeBrightness_enforcePermission(); + DisplayDeviceConfig ddc = + mDisplayDeviceConfigProvider.getDisplayDeviceConfig(displayId); + if (ddc == null) { + throw new IllegalArgumentException( + "Display ID does not have a config for doze-default: " + displayId); + } + return ddc.getDefaultDozeBrightness(); + } } private static boolean isValidBrightness(float brightness) { diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 01604b8bea81..5c1e783c0f52 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -272,7 +272,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private final SettingsObserver mSettingsObserver; // The doze screen brightness. - private final float mScreenBrightnessDozeConfig; + private float mScreenBrightnessDozeConfig; // True if auto-brightness should be used. private boolean mUseSoftwareAutoBrightnessConfig; @@ -550,7 +550,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // DOZE AND DIM SETTINGS mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness( - pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)); + mDisplayDeviceConfig.getDefaultDozeBrightness()); loadBrightnessRampRates(); mSkipScreenOnBrightnessRamp = resources.getBoolean( R.bool.config_skipScreenOnBrightnessRamp); @@ -932,6 +932,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call HighBrightnessModeMetadata hbmMetadata) { // All properties that depend on the associated DisplayDevice and the DDC must be // updated here. + mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness( + mDisplayDeviceConfig.getDefaultDozeBrightness()); loadBrightnessRampRates(); loadNitsRange(mContext.getResources()); setUpAutoBrightness(mContext, mHandler); @@ -1580,7 +1582,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // brightness sources (such as an app override) are not saved to the setting, but should be // reflected in HBM calculations. mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState, - clampedState.getBrightnessMaxReason()); + mBrightnessClamperController.getBrightnessMaxReason()); // Animate the screen brightness when the screen is on or dozing. // Skip the animation when the screen is off or suspended. @@ -1783,7 +1785,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (userSetBrightnessChanged || newEvent.getReason().getReason() != BrightnessReason.REASON_TEMPORARY) { - logBrightnessEvent(newEvent, unthrottledBrightnessState, clampedState); + logBrightnessEvent(newEvent, unthrottledBrightnessState); } if (mBrightnessEventRingBuffer != null) { mBrightnessEventRingBuffer.append(newEvent); @@ -1976,9 +1978,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call synchronized (mCachedBrightnessInfo) { float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX; float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX; - @BrightnessInfo.BrightnessMaxReason int maxReason = - state != null ? state.getBrightnessMaxReason() - : BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; final float minBrightness = Math.max(stateMin, Math.min( mBrightnessRangeController.getCurrentBrightnessMin(), stateMax)); final float maxBrightness = Math.min( @@ -2005,7 +2004,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController.getTransitionPoint()); changed |= mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, - maxReason); + mBrightnessClamperController.getBrightnessMaxReason()); return changed; } } @@ -2905,8 +2904,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN; } - private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness, - DisplayBrightnessState brightnessState) { + private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness) { int modifier = event.getReason().getModifier(); int flags = event.getFlags(); // It's easier to check if the brightness is at maximum level using the brightness @@ -2943,7 +2941,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT, event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0, - brightnessState.getBrightnessMaxReason(), + mBrightnessClamperController.getBrightnessMaxReason(), // TODO: (flc) add brightnessMinReason here too. (modifier & BrightnessReason.MODIFIER_DIMMED) > 0, event.isRbcEnabled(), diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index d3b41b848583..e9ecfc67b7db 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceState; +import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.feature.flags.FeatureFlags; import android.hardware.devicestate.feature.flags.FeatureFlagsImpl; import android.os.Handler; @@ -614,7 +615,11 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { && isBootCompleted && !mFoldSettingProvider.shouldStayAwakeOnFold(); } else { - return mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier()) + return currentState.getIdentifier() + != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER + && pendingState.getIdentifier() + != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER + && mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier()) && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState.getIdentifier()) && isInteractive && isBootCompleted diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index d1fb0091e28b..88d2c007cf37 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -139,7 +139,6 @@ public class BrightnessClamperController { builder.setBrightness(cappedBrightness); builder.setMaxBrightness(mBrightnessCap); builder.setCustomAnimationRate(mCustomAnimationRate); - builder.setBrightnessMaxReason(getBrightnessMaxReason()); if (mClamperType != null) { builder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_THROTTLED); @@ -164,8 +163,19 @@ public class BrightnessClamperController { return builder.build(); } + /** + * See BrightnessThrottler.getBrightnessMaxReason: + * used in: + * 1) DPC2.CachedBrightnessInfo to determine changes + * 2) DPC2.logBrightnessEvent + * 3) HBMController - for logging + * Method is called in mHandler thread (DisplayControllerHandler), in the same thread + * recalculateBrightnessCap and DPC2.updatePowerStateInternal are called. + * Should be moved to DisplayBrightnessState OR derived from DisplayBrightnessState + * TODO: b/263362199 + */ @BrightnessInfo.BrightnessMaxReason - private int getBrightnessMaxReason() { + public int getBrightnessMaxReason() { if (mClamperType == null) { return BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; } else if (mClamperType == Type.THERMAL) { diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 3ce7d2a676b0..e1934b0df3f1 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -154,6 +154,10 @@ public class DisplayManagerFlags { Flags::useFusionProxSensor ); + private final FlagState mDozeBrightnessFloat = new FlagState( + Flags.FLAG_DOZE_BRIGHTNESS_FLOAT, + Flags::dozeBrightnessFloat); + private final FlagState mOffloadControlsDozeAutoBrightness = new FlagState( Flags.FLAG_OFFLOAD_CONTROLS_DOZE_AUTO_BRIGHTNESS, Flags::offloadControlsDozeAutoBrightness @@ -347,6 +351,10 @@ public class DisplayManagerFlags { return mUseFusionProxSensor.getName(); } + public boolean isDozeBrightnessFloatEnabled() { + return mDozeBrightnessFloat.isEnabled(); + } + /** * @return Whether DisplayOffload should control auto-brightness in doze */ @@ -415,6 +423,7 @@ public class DisplayManagerFlags { pw.println(" " + mRefactorDisplayPowerController); pw.println(" " + mResolutionBackupRestore); pw.println(" " + mUseFusionProxSensor); + pw.println(" " + mDozeBrightnessFloat); pw.println(" " + mOffloadControlsDozeAutoBrightness); pw.println(" " + mPeakRefreshRatePhysicalLimit); pw.println(" " + mIgnoreAppPreferredRefreshRate); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index fd3af2383e1b..ac5f97fc3661 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -246,6 +246,14 @@ flag { } flag { + name: "doze_brightness_float" + namespace: "display_manager" + description: "Define doze brightness in the float scale [0, 1]." + bug: "343796384" + is_fixed_read_only: true +} + +flag { name: "offload_controls_doze_auto_brightness" namespace: "display_manager" description: "Allows the registered DisplayOffloader to control if auto-brightness is used in doze" diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java index 5ff421a40e18..7c93c8b80897 100644 --- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -89,8 +89,8 @@ final class DefaultImeVisibilityApplier { void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken, @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason, @UserIdInt int userId) { - final var bindingController = mService.getInputMethodBindingController(userId); final var userData = mService.getUserData(userId); + final var bindingController = userData.mBindingController; final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { if (DEBUG) { @@ -128,9 +128,9 @@ final class DefaultImeVisibilityApplier { void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason, @UserIdInt int userId) { - final var bindingController = mService.getInputMethodBindingController(userId); - final IInputMethodInvoker curMethod = bindingController.getCurMethod(); final var userData = mService.getUserData(userId); + final var bindingController = userData.mBindingController; + final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { // The IME will report its visible state again after the following message finally // delivered to the IME process as an IPC. Hence the inconsistency between @@ -171,8 +171,8 @@ final class DefaultImeVisibilityApplier { void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken, @ImeVisibilityStateComputer.VisibilityState int state, @SoftInputShowHideReason int reason, @UserIdInt int userId) { - final var bindingController = mService.getInputMethodBindingController(userId); final var userData = mService.getUserData(userId); + final var bindingController = userData.mBindingController; final int displayIdToShowIme = bindingController.getDisplayIdToShowIme(); switch (state) { case STATE_SHOW_IME: diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java index 3f28c47a430b..a7280e6e99b4 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -138,6 +138,9 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) boolean isInputMethodPickerShownForTest(); + @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + void onImeSwitchButtonClickFromSystem(int displayId); + InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId); void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes, @@ -344,6 +347,14 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { return mCallback.isInputMethodPickerShownForTest(); } + @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @Override + public void onImeSwitchButtonClickFromSystem(int displayId) { + super.onImeSwitchButtonClickFromSystem_enforcePermission(); + + mCallback.onImeSwitchButtonClickFromSystem(displayId); + } + @Override public InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) { return mCallback.getCurrentInputMethodSubtype(userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index f5faeef9258c..8fd203324d97 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -53,6 +53,7 @@ import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTarge import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; +import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -475,7 +476,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @AnyThread @NonNull - UserDataRepository.UserData getUserData(@UserIdInt int userId) { + UserData getUserData(@UserIdInt int userId) { return mUserDataRepository.getOrCreate(userId); } @@ -1421,15 +1422,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. * Returns true iff the caller is identified to be the current input method with the token. * * @param token the window token given to the input method when it was started - * @param userId userId of the calling IME process + * @param userData {@link UserData} of the calling IME process * @return true if and only if non-null valid token is specified */ @GuardedBy("ImfLock.class") - private boolean calledWithValidTokenLocked(@NonNull IBinder token, @UserIdInt int userId) { + private boolean calledWithValidTokenLocked(@NonNull IBinder token, @NonNull UserData userData) { if (token == null) { throw new InvalidParameterException("token must not be null."); } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; if (token != bindingController.getCurToken()) { Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token." + " uid:" + Binder.getCallingUid() + " token:" + token); @@ -1708,7 +1709,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. clearClientSessionLocked(client); clearClientSessionForAccessibilityLocked(client); // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed. - @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> clientRemovedForUser = + @SuppressWarnings("GuardedBy") Consumer<UserData> clientRemovedForUser = userData -> onClientRemovedInternalLocked(client, userData); mUserDataRepository.forAllUserData(clientRemovedForUser); } @@ -1718,15 +1719,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. */ // TODO(b/325515685): Move this method to InputMethodBindingController @GuardedBy("ImfLock.class") - private void onClientRemovedInternalLocked(ClientState client, - @NonNull UserDataRepository.UserData userData) { + private void onClientRemovedInternalLocked(ClientState client, @NonNull UserData userData) { final int userId = userData.mUserId; if (userData.mCurClient == client) { hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); if (userData.mBoundToMethod) { userData.mBoundToMethod = false; - final var userBindingController = getInputMethodBindingController(userId); + final var userBindingController = userData.mBindingController; IInputMethodInvoker curMethod = userBindingController.getCurMethod(); if (curMethod != null) { // When we unbind input, we are unbinding the client, so we always @@ -1758,7 +1758,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.v(TAG, "unbindCurrentInputLocked: client=" + userData.mCurClient.mClient.asBinder()); } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; if (userData.mBoundToMethod) { userData.mBoundToMethod = false; IInputMethodInvoker curMethod = bindingController.getCurMethod(); @@ -1844,8 +1844,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial, @UserIdInt int userId) { - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; if (!userData.mBoundToMethod) { bindingController.getCurMethod().bindInput(userData.mCurClient.mBinding); userData.mBoundToMethod = true; @@ -2274,9 +2274,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. + bindingController.getCurTokenDisplayId()); } final int userId = bindingController.getUserId(); + final var userData = getUserData(userId); inputMethod.initializeInternal(token, - new InputMethodPrivilegedOperationsImpl(this, token, userId), - getInputMethodNavButtonFlagsLocked(getUserData(userId))); + new InputMethodPrivilegedOperationsImpl(this, token, userData), + getInputMethodNavButtonFlagsLocked(userData)); } @AnyThread @@ -2316,8 +2317,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. channel.dispose(); return; } - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null && method != null && curMethod.asBinder() == method.asBinder()) { @@ -2530,9 +2531,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void updateStatusIcon(@NonNull IBinder token, String packageName, - @DrawableRes int iconId, @UserIdInt int userId) { + @DrawableRes int iconId, @NonNull UserData userData) { + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } final long ident = Binder.clearCallingIdentity(); @@ -2575,8 +2577,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @InputMethodNavButtonFlags - private int getInputMethodNavButtonFlagsLocked( - @NonNull UserDataRepository.UserData userData) { + private int getInputMethodNavButtonFlagsLocked(@NonNull UserData userData) { final int userId = userData.mUserId; final var bindingController = userData.mBindingController; // Whether the current display has a navigation bar. When this is false (e.g. emulator), @@ -2671,14 +2672,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @SuppressWarnings("deprecation") private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition, - @UserIdInt int userId) { + @NonNull UserData userData) { final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId(); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; // Skip update IME status when current token display is not same as focused display. // Note that we still need to update IME status when focusing external display // that does not support system decoration and fallback to show IME on default @@ -2710,9 +2712,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void reportStartInput(@NonNull IBinder token, IBinder startInputToken, - @UserIdInt int userId) { + @NonNull UserData userData) { synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken); @@ -2746,8 +2748,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) { - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; final var curToken = bindingController.getCurToken(); if (curToken == null) { return; @@ -2845,11 +2847,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. settings.putSelectedInputMethod(id); } } - final var bindingController = getInputMethodBindingController(userId); + final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; bindingController.setSelectedMethodId(id); // Also re-initialize controllers. - final var userData = getUserData(userId); userData.mSwitchingController.resetCircularListLocked(mContext, settings); userData.mHardwareKeyboardShortcutController.update(settings); } @@ -2886,7 +2888,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - final var bindingController = getInputMethodBindingController(userId); + final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; if (bindingController.getDeviceIdToShowIme() == DEVICE_ID_DEFAULT) { String ime = SecureSettingsWrapper.getString( Settings.Secure.DEFAULT_INPUT_METHOD, null, userId); @@ -2926,7 +2929,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. resetCurrentMethodAndClientLocked(UnbindReason.NO_IME, userId); } - final var userData = getUserData(userId); userData.mSwitchingController.resetCircularListLocked(mContext, settings); userData.mHardwareKeyboardShortcutController.update(settings); sendOnNavButtonFlagsChangedLocked(userData); @@ -3408,8 +3410,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mVisibilityStateComputer.requestImeVisibility(windowToken, true); // Ensure binding the connection when IME is going to show. - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; bindingController.setCurrentMethodVisible(); final IInputMethodInvoker curMethod = bindingController.getCurMethod(); ImeTracker.forLogging().onCancelled(userData.mCurStatsToken, @@ -3542,7 +3544,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. boolean hideCurrentInputLocked(IBinder windowToken, @NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason, @UserIdInt int userId) { - final var bindingController = getInputMethodBindingController(userId); + final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) { return false; } @@ -3555,7 +3558,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only // IMMS#InputShown indicates that the software keyboard is shown. // TODO(b/246309664): Clean up IMMS#mImeWindowVis - final var userData = getUserData(userId); IInputMethodInvoker curMethod = bindingController.getCurMethod(); final boolean shouldHideSoftInput = curMethod != null && (isInputShownLocked() @@ -3635,6 +3637,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.w(TAG, "User #" + userId + " is not running."); return InputBindResult.INVALID_USER; } + final var userData = getUserData(userId); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.startInputOrWindowGainedFocus"); @@ -3642,7 +3645,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. "InputMethodManagerService#startInputOrWindowGainedFocus", mDumper); final InputBindResult result; synchronized (ImfLock.class) { - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; // If the system is not yet ready, we shouldn't be running third party code. if (!mSystemReady) { return new InputBindResult( @@ -3707,7 +3710,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid); final boolean showForced = mVisibilityStateComputer.mShowForced; - final var userData = getUserData(userId); if (userData.mImeBindingState.mFocusedWindow != windowToken && showForced && shouldClearFlag) { mVisibilityStateComputer.mShowForced = false; @@ -3967,6 +3969,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } + @BinderThread + private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId, + @NonNull UserData userData) { + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(token, userData)) { + return; + } + showInputMethodPickerFromSystem( + InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId); + } + } + + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @Override + public void onImeSwitchButtonClickFromSystem(int displayId) { + showInputMethodPickerFromSystem( + InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId); + } + @NonNull private static IllegalArgumentException getExceptionForUnknownImeId( @Nullable String imeId) { @@ -3974,10 +3995,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void setInputMethod(@NonNull IBinder token, String id, @UserIdInt int userId) { + private void setInputMethod(@NonNull IBinder token, String id, @NonNull UserData userData) { final int callingUid = Binder.getCallingUid(); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); @@ -3992,10 +4014,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void setInputMethodAndSubtype(@NonNull IBinder token, String id, - InputMethodSubtype subtype, @UserIdInt int userId) { + InputMethodSubtype subtype, @NonNull UserData userData) { final int callingUid = Binder.getCallingUid(); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); @@ -4008,18 +4031,20 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. setInputMethodWithSubtypeIdLocked(token, id, SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()), userId); } else { - setInputMethod(token, id, userId); + setInputMethod(token, id, userData); } } } @BinderThread - private boolean switchToPreviousInputMethod(@NonNull IBinder token, @UserIdInt int userId) { + private boolean switchToPreviousInputMethod(@NonNull IBinder token, + @NonNull UserData userData) { + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return false; } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype(); final InputMethodInfo lastImi; @@ -4096,43 +4121,45 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme, - @UserIdInt int userId) { + @NonNull UserData userData) { synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return false; } - return switchToNextInputMethodLocked(token, onlyCurrentIme, userId); + return switchToNextInputMethodLocked(token, onlyCurrentIme, userData); } } @GuardedBy("ImfLock.class") private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme, - @UserIdInt int userId) { - final var bindingController = getInputMethodBindingController(userId); + @NonNull UserData userData) { + final var bindingController = userData.mBindingController; final var currentImi = bindingController.getSelectedMethod(); - final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController + final ImeSubtypeListItem nextSubtype = userData.mSwitchingController .getNextInputMethodLocked(onlyCurrentIme, currentImi, - bindingController.getCurrentSubtype()); + bindingController.getCurrentSubtype(), + MODE_AUTO, true /* forward */); if (nextSubtype == null) { return false; } setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(), - nextSubtype.mSubtypeId, userId); + nextSubtype.mSubtypeId, userData.mUserId); return true; } @BinderThread private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token, - @UserIdInt int userId) { + @NonNull UserData userData) { synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return false; } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; final var currentImi = bindingController.getSelectedMethod(); - final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController + final ImeSubtypeListItem nextSubtype = userData.mSwitchingController .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi, - bindingController.getCurrentSubtype()); + bindingController.getCurrentSubtype(), + MODE_AUTO, true /* forward */); return nextSubtype != null; } } @@ -4530,8 +4557,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void dumpDebug(ProtoOutputStream proto, long fieldId) { synchronized (ImfLock.class) { final int userId = mCurrentUserId; - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; final long token = proto.start(fieldId); proto.write(CUR_METHOD_ID, bindingController.getSelectedMethodId()); proto.write(CUR_SEQ, bindingController.getSequenceNumber()); @@ -4562,12 +4589,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void notifyUserAction(@NonNull IBinder token, @UserIdInt int userId) { + private void notifyUserAction(@NonNull IBinder token, @NonNull UserData userData) { if (DEBUG) { Slog.d(TAG, "Got the notification of a user action."); } synchronized (ImfLock.class) { - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; if (bindingController.getCurToken() != token) { if (DEBUG) { Slog.d(TAG, "Ignoring the user action notification from IMEs that are no longer" @@ -4577,7 +4604,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodInfo imi = bindingController.getSelectedMethod(); if (imi != null) { - getUserData(userId).mSwitchingController.onUserActionLocked(imi, + userData.mSwitchingController.onUserActionLocked(imi, bindingController.getCurrentSubtype()); } } @@ -4585,11 +4612,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible, - @NonNull ImeTracker.Token statsToken, @UserIdInt int userId) { + @NonNull ImeTracker.Token statsToken, @NonNull UserData userData) { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility"); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); return; @@ -4664,8 +4692,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @UserIdInt int userId) { final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken, userId); - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; final WindowManagerInternal.ImeTargetInfo info = mWindowManagerInternal.onToggleImeRequested( show, userData.mImeBindingState.mFocusedWindow, requestToken, @@ -4685,16 +4713,16 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void hideMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason, - @UserIdInt int userId) { + @NonNull UserData userData) { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput"); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); return; } - final var userData = getUserData(userId); ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); final long ident = Binder.clearCallingIdentity(); @@ -4724,16 +4752,16 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void showMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason, - @UserIdInt int userId) { + @NonNull UserData userData) { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput"); + final int userId = userData.mUserId; synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); return; } - final var userData = getUserData(userId); ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); final long ident = Binder.clearCallingIdentity(); @@ -4778,8 +4806,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - void setEnabledSessionLocked(SessionState session, - @NonNull UserDataRepository.UserData userData) { + void setEnabledSessionLocked(SessionState session, @NonNull UserData userData) { if (userData.mEnabledSession != session) { if (userData.mEnabledSession != null && userData.mEnabledSession.mSession != null) { if (DEBUG) Slog.v(TAG, "Disabling: " + userData.mEnabledSession); @@ -4798,7 +4825,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void setEnabledSessionForAccessibilityLocked( SparseArray<AccessibilitySessionState> accessibilitySessions, - @NonNull UserDataRepository.UserData userData) { + @NonNull UserData userData) { // mEnabledAccessibilitySessions could the same object as accessibilitySessions. SparseArray<IAccessibilityInputMethodSession> disabledSessions = new SparseArray<>(); for (int i = 0; i < userData.mEnabledAccessibilitySessions.size(); i++) { @@ -5007,9 +5034,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. case MSG_START_HANDWRITING: final var handwritingRequest = (HandwritingRequest) msg.obj; synchronized (ImfLock.class) { - final int userId = handwritingRequest.userId; - final var bindingController = getInputMethodBindingController(userId); - final var userData = getUserData(userId); + final var userData = handwritingRequest.userData; + final var bindingController = userData.mBindingController; IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod == null || userData.mImeBindingState.mFocusedWindow == null) { return true; @@ -5054,24 +5080,24 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return false; } - private record HandwritingRequest(int requestId, int pid, @UserIdInt int userId) { } + private record HandwritingRequest(int requestId, int pid, @NonNull UserData userData) { } @BinderThread - private void onStylusHandwritingReady(int requestId, int pid, @UserIdInt int userId) { + private void onStylusHandwritingReady(int requestId, int pid, @NonNull UserData userData) { mHandler.obtainMessage(MSG_START_HANDWRITING, - new HandwritingRequest(requestId, pid, userId)).sendToTarget(); + new HandwritingRequest(requestId, pid, userData)).sendToTarget(); } private void handleSetInteractive(final boolean interactive) { synchronized (ImfLock.class) { // TODO(b/305849394): Support multiple IMEs. final int userId = mCurrentUserId; - final var bindingController = getInputMethodBindingController(userId); + final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; mIsInteractive = interactive; updateSystemUiLocked( interactive ? bindingController.getImeWindowVis() : 0, bindingController.getBackDisposition(), userId); - final var userData = getUserData(userId); // Inform the current client of the change in active status if (userData.mCurClient == null || userData.mCurClient.mClient == null) { return; @@ -5303,7 +5329,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - void sendOnNavButtonFlagsChangedLocked(@NonNull UserDataRepository.UserData userData) { + void sendOnNavButtonFlagsChangedLocked(@NonNull UserData userData) { final var bindingController = userData.mBindingController; final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod == null) { @@ -5319,7 +5345,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean value = InputMethodDrawsNavBarResourceMonitor.evaluate(mContext, profileParentId); final var profileUserIds = mUserManagerInternal.getProfileIds(profileParentId, false); - final ArrayList<UserDataRepository.UserData> updatedUsers = new ArrayList<>(); + final ArrayList<UserData> updatedUsers = new ArrayList<>(); for (int profileUserId : profileUserIds) { final var userData = getUserData(profileUserId); userData.mImeDrawsNavBar.set(value); @@ -5457,6 +5483,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Set InputMethod here settings.putSelectedInputMethod(imi != null ? imi.getId() : ""); } + + if (Flags.imeSwitcherRevamp()) { + getUserData(userId).mSwitchingController.onInputMethodSubtypeChanged(); + } } @GuardedBy("ImfLock.class") @@ -5568,20 +5598,38 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private void switchKeyboardLayoutLocked(int direction, @UserIdInt int userId) { + private void switchKeyboardLayoutLocked(int direction, @NonNull UserData userData) { + final int userId = userData.mUserId; final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; final InputMethodInfo currentImi = settings.getMethodMap().get( bindingController.getSelectedMethodId()); if (currentImi == null) { return; } - final InputMethodSubtypeHandle currentSubtypeHandle = - InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype()); - final InputMethodSubtypeHandle nextSubtypeHandle = - getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch( + final var currentSubtype = bindingController.getCurrentSubtype(); + final InputMethodSubtypeHandle nextSubtypeHandle; + if (Flags.imeSwitcherRevamp()) { + final var nextItem = userData.mSwitchingController + .getNextInputMethodForHardware( + false /* onlyCurrentIme */, currentImi, currentSubtype, MODE_AUTO, + direction > 0 /* forward */); + if (nextItem == null) { + Slog.i(TAG, "Hardware keyboard switching shortcut," + + " next input method and subtype not found"); + return; + } + + final var nextSubtype = nextItem.mSubtypeId > NOT_A_SUBTYPE_ID + ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeId) : null; + nextSubtypeHandle = InputMethodSubtypeHandle.of(nextItem.mImi, nextSubtype); + } else { + final InputMethodSubtypeHandle currentSubtypeHandle = + InputMethodSubtypeHandle.of(currentImi, currentSubtype); + nextSubtypeHandle = userData.mHardwareKeyboardShortcutController.onSubtypeSwitch( currentSubtypeHandle, direction > 0); + } if (nextSubtypeHandle == null) { return; } @@ -5727,7 +5775,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (displayId != bindingController.getCurTokenDisplayId()) { return false; } - curHostInputToken = getInputMethodBindingController(userId).getCurHostInputToken(); + curHostInputToken = bindingController.getCurHostInputToken(); if (curHostInputToken == null) { return false; } @@ -5783,8 +5831,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { synchronized (ImfLock.class) { - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; // TODO(b/305829876): Implement user ID verification if (userData.mCurClient != null) { clearClientSessionForAccessibilityLocked(userData.mCurClient, @@ -5821,8 +5869,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId, @UserIdInt int userId) { synchronized (ImfLock.class) { - final var bindingController = getInputMethodBindingController(userId); final var userData = getUserData(userId); + final var bindingController = userData.mBindingController; // TODO(b/305829876): Implement user ID verification if (userData.mCurClient != null) { if (DEBUG) { @@ -5866,14 +5914,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IBinder targetWindowToken) { synchronized (ImfLock.class) { // TODO(b/305849394): Infer userId from displayId - switchKeyboardLayoutLocked(direction, mCurrentUserId); + switchKeyboardLayoutLocked(direction, getUserData(mCurrentUserId)); } } } @BinderThread private IInputContentUriToken createInputContentUriToken(@Nullable IBinder token, - @Nullable Uri contentUri, @Nullable String packageName, @UserIdInt int imeUserId) { + @Nullable Uri contentUri, @Nullable String packageName, @NonNull UserData userData) { if (token == null) { throw new NullPointerException("token"); } @@ -5890,7 +5938,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final int uid = Binder.getCallingUid(); - final var bindingController = getInputMethodBindingController(imeUserId); + final var bindingController = userData.mBindingController; if (bindingController.getSelectedMethodId() == null) { return null; } @@ -5902,7 +5950,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // We cannot simply distinguish a bad IME that reports an arbitrary package name from // an unfortunate IME whose internal state is already obsolete due to the asynchronous // nature of our system. Let's compare it with our internal record. - final var userData = getUserData(imeUserId); final var curPackageName = userData.mCurEditorInfo != null ? userData.mCurEditorInfo.packageName : null; if (!TextUtils.equals(curPackageName, packageName)) { @@ -5914,7 +5961,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int appUserId = UserHandle.getUserId(userData.mCurClient.mUid); // This user ID may be invalid if "contentUri" embedded an invalid user ID. final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri, - imeUserId); + userData.mUserId); final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri); // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid") // actually has the right to grant a read permission for "contentUriWithoutUserId" that @@ -5929,12 +5976,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen, - @UserIdInt int userId) { + @NonNull UserData userData) { synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userId)) { + if (!calledWithValidTokenLocked(token, userData)) { return; } - final var userData = getUserData(userId); if (userData.mCurClient != null && userData.mCurClient.mClient != null) { userData.mInFullscreenMode = fullscreen; userData.mCurClient.mClient.reportFullscreenMode(fullscreen); @@ -6061,8 +6107,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println(" pid=" + c.mPid); }; mClientController.forAllClients(clientControllerDump); - final var bindingController = getInputMethodBindingController(mCurrentUserId); - p.println(" mCurrentUserId=" + mCurrentUserId); + final var bindingController = userData.mBindingController; + p.println(" mCurrentUserId=" + userData.mUserId); p.println(" mCurMethodId=" + bindingController.getSelectedMethodId()); client = userData.mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" @@ -6077,7 +6123,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println(" mUserDataRepository="); // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed. - @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> userDataDump = + @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> { p.println(" mUserId=" + u.mUserId); p.println(" hasMainConnection=" @@ -6614,7 +6660,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 0 /* flags */, SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId); } - final var bindingController = getInputMethodBindingController(userId); + final var bindingController = userData.mBindingController; bindingController.unbindCurrentMethod(); // Enable default IMEs, disable others @@ -6762,26 +6808,26 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private final InputMethodManagerService mImms; @NonNull private final IBinder mToken; - @UserIdInt - private final int mUserId; + @NonNull + private final UserData mUserData; InputMethodPrivilegedOperationsImpl(InputMethodManagerService imms, - @NonNull IBinder token, @UserIdInt int userId) { + @NonNull IBinder token, @NonNull UserData userData) { mImms = imms; mToken = token; - mUserId = userId; + mUserData = userData; } @BinderThread @Override public void setImeWindowStatusAsync(int vis, int backDisposition) { - mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserId); + mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserData); } @BinderThread @Override public void reportStartInputAsync(IBinder startInputToken) { - mImms.reportStartInput(mToken, startInputToken, mUserId); + mImms.reportStartInput(mToken, startInputToken, mUserData); } @BinderThread @@ -6797,7 +6843,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @SuppressWarnings("unchecked") final AndroidFuture<IBinder> typedFuture = future; try { typedFuture.complete(mImms.createInputContentUriToken( - mToken, contentUri, packageName, mUserId).asBinder()); + mToken, contentUri, packageName, mUserData).asBinder()); } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -6806,7 +6852,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void reportFullscreenModeAsync(boolean fullscreen) { - mImms.reportFullscreenMode(mToken, fullscreen, mUserId); + mImms.reportFullscreenMode(mToken, fullscreen, mUserData); } @BinderThread @@ -6814,7 +6860,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void setInputMethod(String id, AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.setInputMethod(mToken, id, mUserId); + mImms.setInputMethod(mToken, id, mUserData); typedFuture.complete(null); } catch (Throwable e) { typedFuture.completeExceptionally(e); @@ -6827,7 +6873,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserId); + mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserData); typedFuture.complete(null); } catch (Throwable e) { typedFuture.completeExceptionally(e); @@ -6841,7 +6887,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserId); + mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserData); typedFuture.complete(null); } catch (Throwable e) { typedFuture.completeExceptionally(e); @@ -6855,7 +6901,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserId); + mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserData); typedFuture.complete(null); } catch (Throwable e) { typedFuture.completeExceptionally(e); @@ -6865,7 +6911,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void updateStatusIconAsync(String packageName, @DrawableRes int iconId) { - mImms.updateStatusIcon(mToken, packageName, iconId, mUserId); + mImms.updateStatusIcon(mToken, packageName, iconId, mUserData); } @BinderThread @@ -6873,7 +6919,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) { @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { - typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserId)); + typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserData)); } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -6886,7 +6932,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme, - mUserId)); + mUserData)); } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -6897,7 +6943,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) { @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { - typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken, mUserId)); + typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken, + mUserData)); } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -6905,21 +6952,27 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override + public void onImeSwitchButtonClickFromClient(int displayId) { + mImms.onImeSwitchButtonClickFromClient(mToken, displayId, mUserData); + } + + @BinderThread + @Override public void notifyUserActionAsync() { - mImms.notifyUserAction(mToken, mUserId); + mImms.notifyUserAction(mToken, mUserData); } @BinderThread @Override public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible, @NonNull ImeTracker.Token statsToken) { - mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserId); + mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserData); } @BinderThread @Override public void onStylusHandwritingReady(int requestId, int pid) { - mImms.onStylusHandwritingReady(requestId, pid, mUserId); + mImms.onStylusHandwritingReady(requestId, pid, mUserData); } @BinderThread @@ -6932,12 +6985,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @Override public void switchKeyboardLayoutAsync(int direction) { synchronized (ImfLock.class) { - if (!mImms.calledWithValidTokenLocked(mToken, mUserId)) { + if (!mImms.calledWithValidTokenLocked(mToken, mUserData)) { return; } final long ident = Binder.clearCallingIdentity(); try { - mImms.switchKeyboardLayoutLocked(direction, mUserId); + mImms.switchKeyboardLayoutLocked(direction, mUserData); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index bb1b9df6cf4c..05cc5985a8cc 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -24,11 +26,14 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Printer; import android.util.Slog; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.VisibleForTesting; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -45,6 +50,34 @@ final class InputMethodSubtypeSwitchingController { private static final boolean DEBUG = false; private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + @IntDef(prefix = {"MODE_"}, value = { + MODE_STATIC, + MODE_RECENT, + MODE_AUTO + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SwitchMode { + } + + /** + * Switch using the static order (the order of the given list of input methods and subtypes). + * This order is only set when given a new list, and never updated. + */ + public static final int MODE_STATIC = 0; + + /** + * Switch using the recency based order, going from most recent to least recent, + * updated on {@link #onUserActionLocked user action}. + */ + public static final int MODE_RECENT = 1; + + /** + * If there was a {@link #onUserActionLocked user action} since the last + * {@link #onInputMethodSubtypeChanged() switch}, and direction is forward, + * use {@link #MODE_RECENT}, otherwise use {@link #MODE_STATIC}. + */ + public static final int MODE_AUTO = 2; + public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { @NonNull @@ -117,20 +150,25 @@ final class InputMethodSubtypeSwitchingController { if (result != 0) { return result; } - // Subtype that has the same locale of the system's has higher priority. - result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0); - if (result != 0) { - return result; - } - // Subtype that has the same language of the system's has higher priority. - result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0); - if (result != 0) { - return result; - } - result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName); - if (result != 0) { - return result; + if (!Flags.imeSwitcherRevamp()) { + // Subtype that has the same locale of the system's has higher priority. + result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0); + if (result != 0) { + return result; + } + // Subtype that has the same language of the system's has higher priority. + result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0); + if (result != 0) { + return result; + } + result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName); + if (result != 0) { + return result; + } } + // This will no longer compare by subtype name, however as {@link Collections.sort} is + // guaranteed to be a stable sorting, this allows sorting by the IME name (and ID), + // while maintaining the order of subtypes (given by each IME) at the IME level. return mImi.getId().compareTo(other.mImi.getId()); } @@ -226,6 +264,59 @@ final class InputMethodSubtypeSwitchingController { return imList; } + @NonNull + private static List<ImeSubtypeListItem> getInputMethodAndSubtypeListForHardwareKeyboard( + @NonNull Context context, @NonNull InputMethodSettings settings) { + if (!Flags.imeSwitcherRevamp()) { + return new ArrayList<>(); + } + final int userId = settings.getUserId(); + final Context userAwareContext = context.getUserId() == userId + ? context + : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); + final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag(); + + final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList(); + if (imis.isEmpty()) { + Slog.w(TAG, "Enabled input method list is empty."); + return new ArrayList<>(); + } + + final ArrayList<ImeSubtypeListItem> imList = new ArrayList<>(); + final int numImes = imis.size(); + for (int i = 0; i < numImes; ++i) { + final InputMethodInfo imi = imis.get(i); + if (!imi.shouldShowInInputMethodPicker()) { + continue; + } + final var subtypes = settings.getEnabledInputMethodSubtypeList(imi, true); + final ArraySet<InputMethodSubtype> enabledSubtypeSet = new ArraySet<>(subtypes); + final CharSequence imeLabel = imi.loadLabel(userAwareContext.getPackageManager()); + if (!subtypes.isEmpty()) { + final int subtypeCount = imi.getSubtypeCount(); + if (DEBUG) { + Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); + } + for (int j = 0; j < subtypeCount; j++) { + final InputMethodSubtype subtype = imi.getSubtypeAt(j); + if (enabledSubtypeSet.contains(subtype) + && subtype.isSuitableForPhysicalKeyboardLayoutMapping()) { + final CharSequence subtypeLabel = + subtype.overridesImplicitlyEnabledSubtype() ? null : subtype + .getDisplayName(userAwareContext, imi.getPackageName(), + imi.getServiceInfo().applicationInfo); + imList.add(new ImeSubtypeListItem(imeLabel, + subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); + } + } + } else { + imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, + mSystemLocaleStr)); + } + } + return imList; + } + private static int calculateSubtypeId(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) @@ -385,6 +476,132 @@ final class InputMethodSubtypeSwitchingController { } } + /** + * List container that allows getting the next item in either forwards or backwards direction, + * in either static or recency order, and either in the same IME or not. + */ + private static class RotationList { + + /** + * List of items in a static order. + */ + @NonNull + private final List<ImeSubtypeListItem> mItems; + + /** + * Mapping of recency index to static index (in {@link #mItems}), with lower indices being + * more recent. + */ + @NonNull + private final int[] mRecencyMap; + + RotationList(@NonNull List<ImeSubtypeListItem> items) { + mItems = items; + mRecencyMap = new int[items.size()]; + for (int i = 0; i < mItems.size(); i++) { + mRecencyMap[i] = i; + } + } + + /** + * Gets the next input method and subtype from the given ones. + * + * @param imi the input method to find the next value from. + * @param subtype the input method subtype to find the next value from, if any. + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param useRecency whether to use the recency order, or the static order. + * @param forward whether to search forwards to backwards in the list. + * @return the next input method and subtype if found, otherwise {@code null}. + */ + @Nullable + public ImeSubtypeListItem next(@NonNull InputMethodInfo imi, + @Nullable InputMethodSubtype subtype, boolean onlyCurrentIme, + boolean useRecency, boolean forward) { + final int size = mItems.size(); + if (size <= 1) { + return null; + } + final int index = getIndex(imi, subtype, useRecency); + if (index < 0) { + return null; + } + + final int incrementSign = (forward ? 1 : -1); + + for (int i = 1; i < size; i++) { + final int nextIndex = (index + i * incrementSign + size) % size; + final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex; + final var nextItem = mItems.get(mappedIndex); + if (!onlyCurrentIme || nextItem.mImi.equals(imi)) { + return nextItem; + } + } + return null; + } + + /** + * Sets the given input method and subtype as the most recent one. + * + * @param imi the input method to set as the most recent. + * @param subtype the input method subtype to set as the most recent, if any. + * @return {@code true} if the recency was updated, otherwise {@code false}. + */ + public boolean setMostRecent(@NonNull InputMethodInfo imi, + @Nullable InputMethodSubtype subtype) { + if (mItems.size() <= 1) { + return false; + } + + final int recencyIndex = getIndex(imi, subtype, true /* useRecency */); + if (recencyIndex <= 0) { + // Already most recent or not found. + return false; + } + final int staticIndex = mRecencyMap[recencyIndex]; + System.arraycopy(mRecencyMap, 0, mRecencyMap, 1, recencyIndex); + mRecencyMap[0] = staticIndex; + return true; + } + + /** + * Gets the index of the given input method and subtype, in either recency or static order. + * + * @param imi the input method to get the index of. + * @param subtype the input method subtype to get the index of, if any. + * @param useRecency whether to get the index in the recency or static order. + * @return an index in either {@link #mItems} or {@link #mRecencyMap}, or {@code -1} + * if not found. + */ + @IntRange(from = -1) + private int getIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype, + boolean useRecency) { + final int subtypeIndex = calculateSubtypeId(imi, subtype); + for (int i = 0; i < mItems.size(); i++) { + final int mappedIndex = useRecency ? mRecencyMap[i] : i; + final var item = mItems.get(mappedIndex); + if (item.mImi.equals(imi) && item.mSubtypeId == subtypeIndex) { + return i; + } + } + return -1; + } + + /** Dumps the state of the list into the given printer. */ + private void dump(@NonNull Printer pw, @NonNull String prefix) { + pw.println(prefix + "Static order:"); + for (int i = 0; i < mItems.size(); ++i) { + final var item = mItems.get(i); + pw.println(prefix + "i=" + i + " item=" + item); + } + pw.println(prefix + "Recency order:"); + for (int i = 0; i < mRecencyMap.length; ++i) { + final int index = mRecencyMap[i]; + final var item = mItems.get(index); + pw.println(prefix + "i=" + i + " item=" + item); + } + } + } + @VisibleForTesting public static class ControllerImpl { @@ -392,10 +609,23 @@ final class InputMethodSubtypeSwitchingController { private final DynamicRotationList mSwitchingAwareRotationList; @NonNull private final StaticRotationList mSwitchingUnawareRotationList; + /** List of input methods and subtypes. */ + @Nullable + private final RotationList mRotationList; + /** List of input methods and subtypes suitable for hardware keyboards. */ + @Nullable + private final RotationList mHardwareRotationList; + + /** + * Whether there was a user action since the last input method and subtype switch. + * Used to determine the switching behaviour for {@link #MODE_AUTO}. + */ + private boolean mUserActionSinceSwitch; @NonNull public static ControllerImpl createFrom(@Nullable ControllerImpl currentInstance, - @NonNull List<ImeSubtypeListItem> sortedEnabledItems) { + @NonNull List<ImeSubtypeListItem> sortedEnabledItems, + @NonNull List<ImeSubtypeListItem> hardwareKeyboardItems) { final var switchingAwareImeSubtypes = filterImeSubtypeList(sortedEnabledItems, true /* supportsSwitchingToNextInputMethod */); final var switchingUnawareImeSubtypes = filterImeSubtypeList(sortedEnabledItems, @@ -421,22 +651,55 @@ final class InputMethodSubtypeSwitchingController { switchingUnawareRotationList = new StaticRotationList(switchingUnawareImeSubtypes); } - return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList); + final RotationList rotationList; + if (!Flags.imeSwitcherRevamp()) { + rotationList = null; + } else if (currentInstance != null && currentInstance.mRotationList != null + && Objects.equals( + currentInstance.mRotationList.mItems, sortedEnabledItems)) { + // Can reuse the current instance. + rotationList = currentInstance.mRotationList; + } else { + rotationList = new RotationList(sortedEnabledItems); + } + + final RotationList hardwareRotationList; + if (!Flags.imeSwitcherRevamp()) { + hardwareRotationList = null; + } else if (currentInstance != null && currentInstance.mHardwareRotationList != null + && Objects.equals( + currentInstance.mHardwareRotationList.mItems, hardwareKeyboardItems)) { + // Can reuse the current instance. + hardwareRotationList = currentInstance.mHardwareRotationList; + } else { + hardwareRotationList = new RotationList(hardwareKeyboardItems); + } + + return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList, + rotationList, hardwareRotationList); } private ControllerImpl(@NonNull DynamicRotationList switchingAwareRotationList, - @NonNull StaticRotationList switchingUnawareRotationList) { + @NonNull StaticRotationList switchingUnawareRotationList, + @Nullable RotationList rotationList, + @Nullable RotationList hardwareRotationList) { mSwitchingAwareRotationList = switchingAwareRotationList; mSwitchingUnawareRotationList = switchingUnawareRotationList; + mRotationList = rotationList; + mHardwareRotationList = hardwareRotationList; } @Nullable public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, - @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { + @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype, + @SwitchMode int mode, boolean forward) { if (imi == null) { return null; } - if (imi.supportsSwitchingToNextInputMethod()) { + if (Flags.imeSwitcherRevamp() && mRotationList != null) { + return mRotationList.next(imi, subtype, onlyCurrentIme, + isRecency(mode, forward), forward); + } else if (imi.supportsSwitchingToNextInputMethod()) { return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, subtype); } else { @@ -445,11 +708,66 @@ final class InputMethodSubtypeSwitchingController { } } - public void onUserActionLocked(@NonNull InputMethodInfo imi, + @Nullable + public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme, + @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype, + @SwitchMode int mode, boolean forward) { + if (Flags.imeSwitcherRevamp() && mHardwareRotationList != null) { + return mHardwareRotationList.next(imi, subtype, onlyCurrentIme, + isRecency(mode, forward), forward); + } + return null; + } + + /** + * Called when the user took an action that should update the recency of the current + * input method and subtype in the switching list. + * + * @param imi the currently selected input method. + * @param subtype the currently selected input method subtype, if any. + * @return {@code true} if the recency was updated, otherwise {@code false}. + * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary() + */ + public boolean onUserActionLocked(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - if (imi.supportsSwitchingToNextInputMethod()) { + boolean recencyUpdated = false; + if (Flags.imeSwitcherRevamp()) { + if (mRotationList != null) { + recencyUpdated |= mRotationList.setMostRecent(imi, subtype); + } + if (mHardwareRotationList != null) { + recencyUpdated |= mHardwareRotationList.setMostRecent(imi, subtype); + } + if (recencyUpdated) { + mUserActionSinceSwitch = true; + } + } else if (imi.supportsSwitchingToNextInputMethod()) { mSwitchingAwareRotationList.onUserAction(imi, subtype); } + return recencyUpdated; + } + + /** Called when the input method and subtype was changed. */ + public void onInputMethodSubtypeChanged() { + mUserActionSinceSwitch = false; + } + + /** + * Whether the given mode and direction result in recency or static order. + * + * <p>{@link #MODE_AUTO} resolves to the recency order for the first forwards switch + * after an {@link #onUserActionLocked user action}, and otherwise to the static order.</p> + * + * @param mode the switching mode. + * @param forward the switching direction. + * @return {@code true} for the recency order, otherwise {@code false}. + */ + private boolean isRecency(@SwitchMode int mode, boolean forward) { + if (mode == MODE_AUTO && mUserActionSinceSwitch && forward) { + return true; + } else { + return mode == MODE_RECENT; + } } @NonNull @@ -473,6 +791,17 @@ final class InputMethodSubtypeSwitchingController { mSwitchingAwareRotationList.dump(pw, prefix + " "); pw.println(prefix + "mSwitchingUnawareRotationList:"); mSwitchingUnawareRotationList.dump(pw, prefix + " "); + if (Flags.imeSwitcherRevamp()) { + if (mRotationList != null) { + pw.println(prefix + "mRotationList:"); + mRotationList.dump(pw, prefix + " "); + } + if (mHardwareRotationList != null) { + pw.println(prefix + "mHardwareRotationList:"); + mHardwareRotationList.dump(pw, prefix + " "); + } + pw.println("User action since last switch: " + mUserActionSinceSwitch); + } } } @@ -480,26 +809,71 @@ final class InputMethodSubtypeSwitchingController { private ControllerImpl mController; InputMethodSubtypeSwitchingController() { - mController = ControllerImpl.createFrom(null, Collections.emptyList()); + mController = ControllerImpl.createFrom(null, Collections.emptyList(), + Collections.emptyList()); } + /** + * Called when the user took an action that should update the recency of the current + * input method and subtype in the switching list. + * + * @param imi the currently selected input method. + * @param subtype the currently selected input method subtype, if any. + * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary() + */ public void onUserActionLocked(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { mController.onUserActionLocked(imi, subtype); } + /** Called when the input method and subtype was changed. */ + public void onInputMethodSubtypeChanged() { + mController.onInputMethodSubtypeChanged(); + } + public void resetCircularListLocked(@NonNull Context context, @NonNull InputMethodSettings settings) { mController = ControllerImpl.createFrom(mController, getSortedInputMethodAndSubtypeList( false /* includeAuxiliarySubtypes */, false /* isScreenLocked */, - false /* forImeMenu */, context, settings)); + false /* forImeMenu */, context, settings), + getInputMethodAndSubtypeListForHardwareKeyboard(context, settings)); } + /** + * Gets the next input method and subtype, starting from the given ones, in the given direction. + * + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param imi the input method to find the next value from. + * @param subtype the input method subtype to find the next value from, if any. + * @param mode the switching mode. + * @param forward whether to search search forwards or backwards in the list. + * @return the next input method and subtype if found, otherwise {@code null}. + */ @Nullable public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, - @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - return mController.getNextInputMethod(onlyCurrentIme, imi, subtype); + @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype, + @SwitchMode int mode, boolean forward) { + return mController.getNextInputMethod(onlyCurrentIme, imi, subtype, mode, forward); + } + + /** + * Gets the next input method and subtype suitable for hardware keyboards, starting from the + * given ones, in the given direction. + * + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param imi the input method to find the next value from. + * @param subtype the input method subtype to find the next value from, if any. + * @param mode the switching mode + * @param forward whether to search search forwards or backwards in the list. + * @return the next input method and subtype if found, otherwise {@code null}. + */ + @Nullable + public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme, + @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype, + @SwitchMode int mode, boolean forward) { + return mController.getNextInputMethodForHardware(onlyCurrentIme, imi, subtype, mode, + forward); } public void dump(@NonNull Printer pw, @NonNull String prefix) { diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java new file mode 100644 index 000000000000..ec5c9e6a3550 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/UserData.java @@ -0,0 +1,147 @@ +/* + * 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.server.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.util.SparseArray; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ImeTracker; +import android.window.ImeOnBackInvokedDispatcher; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; +import com.android.internal.inputmethod.IRemoteInputConnection; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** Placeholder for all IMMS user specific fields */ +final class UserData { + @UserIdInt + final int mUserId; + + @NonNull + final InputMethodBindingController mBindingController; + + @NonNull + final InputMethodSubtypeSwitchingController mSwitchingController = + new InputMethodSubtypeSwitchingController(); + + @NonNull + final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController = + new HardwareKeyboardShortcutController(); + + /** + * Have we called mCurMethod.bindInput()? + */ + @GuardedBy("ImfLock.class") + boolean mBoundToMethod = false; + + /** + * Have we called bindInput() for accessibility services? + */ + @GuardedBy("ImfLock.class") + boolean mBoundToAccessibility; + + @GuardedBy("ImfLock.class") + @NonNull + ImeBindingState mImeBindingState = ImeBindingState.newEmptyState(); + + @GuardedBy("ImfLock.class") + @Nullable + ClientState mCurClient = null; + + @GuardedBy("ImfLock.class") + boolean mInFullscreenMode; + + /** + * The {@link IRemoteInputConnection} last provided by the current client. + */ + @GuardedBy("ImfLock.class") + @Nullable + IRemoteInputConnection mCurInputConnection; + + /** + * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to + * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME. + */ + @GuardedBy("ImfLock.class") + @Nullable + ImeOnBackInvokedDispatcher mCurImeDispatcher; + + /** + * The {@link IRemoteAccessibilityInputConnection} last provided by the current client. + */ + @GuardedBy("ImfLock.class") + @Nullable + IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection; + + /** + * The {@link EditorInfo} last provided by the current client. + */ + @GuardedBy("ImfLock.class") + @Nullable + EditorInfo mCurEditorInfo; + + /** + * The token tracking the current IME show request that is waiting for a connection to an + * IME, otherwise {@code null}. + */ + @GuardedBy("ImfLock.class") + @Nullable + ImeTracker.Token mCurStatsToken; + + /** + * Currently enabled session. + */ + @GuardedBy("ImfLock.class") + @Nullable + InputMethodManagerService.SessionState mEnabledSession; + + @GuardedBy("ImfLock.class") + @Nullable + SparseArray<InputMethodManagerService.AccessibilitySessionState> + mEnabledAccessibilitySessions = new SparseArray<>(); + + /** + * A per-user cache of {@link InputMethodSettings#getEnabledInputMethodsStr()}. + */ + @GuardedBy("ImfLock.class") + @NonNull + String mLastEnabledInputMethodsStr = ""; + + /** + * {@code true} when the IME is responsible for drawing the navigation bar and its buttons. + */ + @NonNull + final AtomicBoolean mImeDrawsNavBar = new AtomicBoolean(); + + /** + * Intended to be instantiated only from this file. + */ + UserData(@UserIdInt int userId, + @NonNull InputMethodBindingController bindingController) { + mUserId = userId; + mBindingController = bindingController; + } + + @Override + public String toString() { + return "UserData{" + "mUserId=" + mUserId + '}'; + } +} diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java index 7c68d547ecd3..6f831cc29026 100644 --- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java +++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java @@ -18,18 +18,11 @@ package com.android.server.inputmethod; import android.annotation.AnyThread; import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.UserIdInt; import android.util.SparseArray; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ImeTracker; -import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; -import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; -import com.android.internal.inputmethod.IRemoteInputConnection; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; import java.util.function.IntFunction; @@ -87,120 +80,4 @@ final class UserDataRepository { mUserDataLock.writeLock().unlock(); } } - - /** Placeholder for all IMMS user specific fields */ - static final class UserData { - @UserIdInt - final int mUserId; - - @NonNull - final InputMethodBindingController mBindingController; - - @NonNull - final InputMethodSubtypeSwitchingController mSwitchingController; - - @NonNull - final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController; - - /** - * Have we called mCurMethod.bindInput()? - */ - @GuardedBy("ImfLock.class") - boolean mBoundToMethod = false; - - /** - * Have we called bindInput() for accessibility services? - */ - @GuardedBy("ImfLock.class") - boolean mBoundToAccessibility; - - @GuardedBy("ImfLock.class") - @NonNull - ImeBindingState mImeBindingState = ImeBindingState.newEmptyState(); - - @GuardedBy("ImfLock.class") - @Nullable - ClientState mCurClient = null; - - @GuardedBy("ImfLock.class") - boolean mInFullscreenMode; - - /** - * The {@link IRemoteInputConnection} last provided by the current client. - */ - @GuardedBy("ImfLock.class") - @Nullable - IRemoteInputConnection mCurInputConnection; - - /** - * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to - * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME. - */ - @GuardedBy("ImfLock.class") - @Nullable - ImeOnBackInvokedDispatcher mCurImeDispatcher; - - /** - * The {@link IRemoteAccessibilityInputConnection} last provided by the current client. - */ - @GuardedBy("ImfLock.class") - @Nullable - IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection; - - /** - * The {@link EditorInfo} last provided by the current client. - */ - @GuardedBy("ImfLock.class") - @Nullable - EditorInfo mCurEditorInfo; - - /** - * The token tracking the current IME show request that is waiting for a connection to an - * IME, otherwise {@code null}. - */ - @GuardedBy("ImfLock.class") - @Nullable - ImeTracker.Token mCurStatsToken; - - /** - * Currently enabled session. - */ - @GuardedBy("ImfLock.class") - @Nullable - InputMethodManagerService.SessionState mEnabledSession; - - @GuardedBy("ImfLock.class") - @Nullable - SparseArray<InputMethodManagerService.AccessibilitySessionState> - mEnabledAccessibilitySessions = new SparseArray<>(); - - /** - * A per-user cache of {@link InputMethodSettings#getEnabledInputMethodsStr()}. - */ - @GuardedBy("ImfLock.class") - @NonNull - String mLastEnabledInputMethodsStr = ""; - - /** - * {@code true} when the IME is responsible for drawing the navigation bar and its buttons. - */ - @NonNull - final AtomicBoolean mImeDrawsNavBar = new AtomicBoolean(); - - /** - * Intended to be instantiated only from this file. - */ - private UserData(@UserIdInt int userId, - @NonNull InputMethodBindingController bindingController) { - mUserId = userId; - mBindingController = bindingController; - mSwitchingController = new InputMethodSubtypeSwitchingController(); - mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(); - } - - @Override - public String toString() { - return "UserData{" + "mUserId=" + mUserId + '}'; - } - } } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 41aac32ad08f..770e12d8e49a 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -296,6 +296,12 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { return mInner.isInputMethodPickerShownForTest(); } + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @Override + public void onImeSwitchButtonClickFromSystem(int displayId) { + mInner.onImeSwitchButtonClickFromSystem(displayId); + } + @Override public InputMethodSubtype getCurrentInputMethodSubtype(int userId) { return mInner.getCurrentInputMethodSubtype(userId); diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 4851a81d3b69..3d0b079c69c8 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -48,6 +48,7 @@ import android.util.AtomicFile; import android.util.Slog; import android.util.Xml; +import com.android.internal.annotations.KeepForWeakReference; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.util.FrameworkStatsLog; @@ -100,6 +101,7 @@ public class LocaleManagerService extends SystemService { private LocaleManagerBackupHelper mBackupHelper; + @KeepForWeakReference private final PackageMonitor mPackageMonitor; private final Object mWriteLock = new Object(); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9e53cc357ea4..016abff88299 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -110,8 +110,8 @@ import static android.service.notification.Adjustment.TYPE_PROMOTION; import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA; import static android.service.notification.Flags.callstyleCallbackApi; import static android.service.notification.Flags.notificationForceGrouping; -import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle; +import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -248,7 +248,6 @@ import android.content.pm.UserInfo; import android.content.pm.VersionedPackage; import android.content.res.Resources; import android.database.ContentObserver; -import android.graphics.drawable.Icon; import android.metrics.LogMaker; import android.net.Uri; import android.os.Binder; @@ -373,7 +372,6 @@ import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.BackgroundActivityStartCallback; import com.android.server.wm.WindowManagerInternal; -import java.util.function.BiPredicate; import libcore.io.IoUtils; import org.json.JSONException; @@ -4634,29 +4632,28 @@ public class NotificationManagerService extends SystemService { @Override public List<String> getPackagesBypassingDnd(int userId, - boolean includeConversationChannels) { + boolean includeConversationChannels) { checkCallerIsSystem(); final ArraySet<String> packageNames = new ArraySet<>(); - for (int user : mUm.getProfileIds(userId, false)) { - List<PackageInfo> pkgs = mPackageManagerClient.getInstalledPackagesAsUser(0, user); - for (PackageInfo pi : pkgs) { - String pkg = pi.packageName; - // If any NotificationChannel for this package is bypassing, the - // package is considered bypassing. - for (NotificationChannel channel : getNotificationChannelsBypassingDnd(pkg, - pi.applicationInfo.uid).getList()) { - // Skips non-demoted conversation channels. - if (!includeConversationChannels - && !TextUtils.isEmpty(channel.getConversationId()) - && !channel.isDemoted()) { - continue; - } - packageNames.add(pkg); + List<PackageInfo> pkgs = mPackageManagerClient.getInstalledPackagesAsUser(0, userId); + for (PackageInfo pi : pkgs) { + String pkg = pi.packageName; + // If any NotificationChannel for this package is bypassing, the + // package is considered bypassing. + for (NotificationChannel channel : getNotificationChannelsBypassingDnd(pkg, + pi.applicationInfo.uid).getList()) { + // Skips non-demoted conversation channels. + if (!includeConversationChannels + && !TextUtils.isEmpty(channel.getConversationId()) + && !channel.isDemoted()) { + continue; } + packageNames.add(pkg); } } + return new ArrayList<String>(packageNames); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 303371bd9a92..8d3f07edb687 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2261,6 +2261,12 @@ final class InstallPackageHelper { installRequest.getNewUsers()); mPm.updateSequenceNumberLP(ps, installRequest.getNewUsers()); mPm.updateInstantAppInstallerLocked(packageName); + + // The installation is success, remove the split info copy stored in package + // setting for the downgrade version check of DELETE_KEEP_DATA and archived app + // cases. + ps.setSplitNames(null); + ps.setSplitRevisionCodes(null); } installRequest.onCommitFinished(); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index c3cac2032a91..a1dffc6c25be 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -1423,11 +1423,8 @@ public class PackageManagerServiceUtils { */ public static void checkDowngrade(@NonNull PackageSetting before, @NonNull PackageInfoLite after) throws PackageManagerException { - if (after.getLongVersionCode() < before.getVersionCode()) { - throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, - "Update version code " + after.versionCode + " is older than current " - + before.getVersionCode()); - } + checkDowngrade(before.getVersionCode(), before.getBaseRevisionCode(), + before.getSplitNames(), before.getSplitRevisionCodes(), after); } /** @@ -1436,28 +1433,35 @@ public class PackageManagerServiceUtils { */ public static void checkDowngrade(@NonNull AndroidPackage before, @NonNull PackageInfoLite after) throws PackageManagerException { - if (after.getLongVersionCode() < before.getLongVersionCode()) { + checkDowngrade(before.getLongVersionCode(), before.getBaseRevisionCode(), + before.getSplitNames(), before.getSplitRevisionCodes(), after); + } + + private static void checkDowngrade(long beforeVersionCode, int beforeBaseRevisionCode, + @NonNull String[] beforeSplitNames, @NonNull int[] beforeSplitRevisionCodes, + @NonNull PackageInfoLite after) throws PackageManagerException { + if (after.getLongVersionCode() < beforeVersionCode) { throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, "Update version code " + after.versionCode + " is older than current " - + before.getLongVersionCode()); - } else if (after.getLongVersionCode() == before.getLongVersionCode()) { - if (after.baseRevisionCode < before.getBaseRevisionCode()) { + + beforeVersionCode); + } else if (after.getLongVersionCode() == beforeVersionCode) { + if (after.baseRevisionCode < beforeBaseRevisionCode) { throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, "Update base revision code " + after.baseRevisionCode - + " is older than current " + before.getBaseRevisionCode()); + + " is older than current " + beforeBaseRevisionCode); } if (!ArrayUtils.isEmpty(after.splitNames)) { for (int i = 0; i < after.splitNames.length; i++) { final String splitName = after.splitNames[i]; - final int j = ArrayUtils.indexOf(before.getSplitNames(), splitName); + final int j = ArrayUtils.indexOf(beforeSplitNames, splitName); if (j != -1) { - if (after.splitRevisionCodes[i] < before.getSplitRevisionCodes()[j]) { + if (after.splitRevisionCodes[i] < beforeSplitRevisionCodes[j]) { throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, "Update split " + splitName + " revision code " + after.splitRevisionCodes[i] + " is older than current " - + before.getSplitRevisionCodes()[j]); + + beforeSplitRevisionCodes[j]); } } } diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 82df527edcc3..9f10e0166120 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -234,6 +234,22 @@ public class PackageSetting extends SettingBase implements PackageStateInternal @Nullable private byte[] mRestrictUpdateHash; + // This is the copy of the same data stored in AndroidPackage. It is not null if the + // AndroidPackage is deleted in cases of DELETE_KEEP_DATA. When AndroidPackage is not null, + // the field will be null, and the getter method will return the data from AndroidPackage + // instead. + @Nullable + private String[] mSplitNames; + + // This is the copy of the same data stored in AndroidPackage. It is not null if the + // AndroidPackage is deleted in cases of DELETE_KEEP_DATA. When AndroidPackage is not null, + // the field will be null, and the getter method will return the data from AndroidPackage + // instead. + @Nullable + private int[] mSplitRevisionCodes; + + private int mBaseRevisionCode; + /** * Snapshot support. */ @@ -578,6 +594,62 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return getBoolean(Booleans.DEBUGGABLE); } + /** + * @see AndroidPackage#getBaseRevisionCode + */ + public PackageSetting setBaseRevisionCode(int value) { + mBaseRevisionCode = value; + onChanged(); + return this; + } + + /** + * @see AndroidPackage#getBaseRevisionCode + */ + public int getBaseRevisionCode() { + return mBaseRevisionCode; + } + + /** + * @see AndroidPackage#getSplitNames + */ + public PackageSetting setSplitNames(String[] value) { + mSplitNames = value; + onChanged(); + return this; + } + + /** + * @see AndroidPackage#getSplitNames + */ + @NonNull + public String[] getSplitNames() { + if (pkg != null) { + return pkg.getSplitNames(); + } + return mSplitNames == null ? EmptyArray.STRING : mSplitNames; + } + + /** + * @see AndroidPackage#getSplitRevisionCodes + */ + public PackageSetting setSplitRevisionCodes(int[] value) { + mSplitRevisionCodes = value; + onChanged(); + return this; + } + + /** + * @see AndroidPackage#getSplitRevisionCodes + */ + @NonNull + public int[] getSplitRevisionCodes() { + if (pkg != null) { + return pkg.getSplitRevisionCodes(); + } + return mSplitRevisionCodes == null ? EmptyArray.INT : mSplitRevisionCodes; + } + @Override public String toString() { return "PackageSetting{" @@ -739,6 +811,11 @@ public class PackageSetting extends SettingBase implements PackageStateInternal mTargetSdkVersion = other.mTargetSdkVersion; mRestrictUpdateHash = other.mRestrictUpdateHash == null ? null : other.mRestrictUpdateHash.clone(); + mBaseRevisionCode = other.mBaseRevisionCode; + mSplitNames = other.mSplitNames != null + ? Arrays.copyOf(other.mSplitNames, other.mSplitNames.length) : null; + mSplitRevisionCodes = other.mSplitRevisionCodes != null + ? Arrays.copyOf(other.mSplitRevisionCodes, other.mSplitRevisionCodes.length) : null; usesSdkLibraries = other.usesSdkLibraries != null ? Arrays.copyOf(other.usesSdkLibraries, diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 2f2c451ca7f3..7afc35819aa7 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -432,6 +432,13 @@ final class RemovePackageHelper { } deletedPs.setInstalled(/* installed= */ false, userId); } + + // Preserve split apk information for downgrade check with DELETE_KEEP_DATA and archived + // app cases + if (deletedPkg.getSplitNames() != null) { + deletedPs.setSplitNames(deletedPkg.getSplitNames()); + deletedPs.setSplitRevisionCodes(deletedPkg.getSplitRevisionCodes()); + } } // make sure to preserve per-user installed state if this removal was just diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java index d8ce38e0cd2c..95561f5fe0e3 100644 --- a/services/core/java/com/android/server/pm/ScanPackageUtils.java +++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java @@ -437,8 +437,9 @@ final class ScanPackageUtils { pkgSetting.setIsOrphaned(true); } - // update debuggable to packageSetting + // update debuggable and BaseRevisionCode to packageSetting pkgSetting.setDebuggable(parsedPackage.isDebuggable()); + pkgSetting.setBaseRevisionCode(parsedPackage.getBaseRevisionCode()); // Take care of first install / last update times. final long scanFileTime = getLastModifiedTime(parsedPackage); diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 0d16b009d9a5..9177e2b75891 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -325,6 +325,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile private static final String TAG_MIME_TYPE = "mime-type"; private static final String TAG_ARCHIVE_STATE = "archive-state"; private static final String TAG_ARCHIVE_ACTIVITY_INFO = "archive-activity-info"; + private static final String TAG_SPLIT_VERSION = "split-version"; public static final String ATTR_NAME = "name"; public static final String ATTR_PACKAGE = "package"; @@ -3261,6 +3262,9 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile if (pkg.isLoading()) { serializer.attributeBoolean(null, "isLoading", true); } + if (pkg.getBaseRevisionCode() != 0) { + serializer.attributeInt(null, "baseRevisionCode", pkg.getBaseRevisionCode()); + } serializer.attributeFloat(null, "loadingProgress", pkg.getLoadingProgress()); serializer.attributeLongHex(null, "loadingCompletedTime", pkg.getLoadingCompletedTime()); @@ -3289,7 +3293,12 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile writeUpgradeKeySetsLPr(serializer, pkg.getKeySetData()); writeKeySetAliasesLPr(serializer, pkg.getKeySetData()); writeMimeGroupLPr(serializer, pkg.getMimeGroups()); - + // If getPkg is not NULL, these values are from the getPkg. And these values are preserved + // for the downgrade check for DELETE_KEEP_DATA and archived app cases. If the getPkg is + // not NULL, we don't need to preserve it. + if (pkg.getPkg() == null) { + writeSplitVersionsLPr(serializer, pkg.getSplitNames(), pkg.getSplitRevisionCodes()); + } serializer.endTag(null, "package"); } @@ -4071,6 +4080,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile byte[] restrictUpdateHash = null; boolean isScannedAsStoppedSystemApp = false; boolean isSdkLibrary = false; + int baseRevisionCode = 0; try { name = parser.getAttributeValue(null, ATTR_NAME); realName = parser.getAttributeValue(null, "realName"); @@ -4116,6 +4126,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile appMetadataFilePath = parser.getAttributeValue(null, "appMetadataFilePath"); appMetadataSource = parser.getAttributeInt(null, "appMetadataSource", PackageManager.APP_METADATA_SOURCE_UNKNOWN); + baseRevisionCode = parser.getAttributeInt(null, "baseRevisionCode", 0); isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null, "scannedAsStoppedSystemApp", false); @@ -4269,6 +4280,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile .setAppMetadataFilePath(appMetadataFilePath) .setAppMetadataSource(appMetadataSource) .setTargetSdkVersion(targetSdkVersion) + .setBaseRevisionCode(baseRevisionCode) .setRestrictUpdateHash(restrictUpdateHash) .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp); // Handle legacy string here for single-user mode @@ -4374,6 +4386,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile readUsesStaticLibLPw(parser, packageSetting); } else if (tagName.equals(TAG_USES_SDK_LIB)) { readUsesSdkLibLPw(parser, packageSetting); + } else if (tagName.equals(TAG_SPLIT_VERSION)) { + readSplitVersionsLPw(parser, packageSetting); } else { PackageManagerService.reportSettingsProblem(Log.WARN, "Unknown element under <package>: " + parser.getName()); @@ -4470,6 +4484,37 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } } + private void readSplitVersionsLPw(TypedXmlPullParser parser, PackageSetting outPs) + throws IOException, XmlPullParserException { + String splitName = parser.getAttributeValue(null, ATTR_NAME); + int splitRevision = parser.getAttributeInt(null, ATTR_VERSION, -1); + if (splitName != null && splitRevision >= 0) { + outPs.setSplitNames(ArrayUtils.appendElement(String.class, + outPs.getSplitNames(), splitName)); + outPs.setSplitRevisionCodes(ArrayUtils.appendInt( + outPs.getSplitRevisionCodes(), splitRevision)); + } + + XmlUtils.skipCurrentTag(parser); + } + + private void writeSplitVersionsLPr(TypedXmlSerializer serializer, String[] splitNames, + int[] splitRevisionCodes) throws IOException { + if (ArrayUtils.isEmpty(splitNames) || ArrayUtils.isEmpty(splitRevisionCodes) + || splitNames.length != splitRevisionCodes.length) { + return; + } + final int libLength = splitNames.length; + for (int i = 0; i < libLength; i++) { + final String splitName = splitNames[i]; + final int splitRevision = splitRevisionCodes[i]; + serializer.startTag(null, TAG_SPLIT_VERSION); + serializer.attribute(null, ATTR_NAME, splitName); + serializer.attributeInt(null, ATTR_VERSION, splitRevision); + serializer.endTag(null, TAG_SPLIT_VERSION); + } + } + private void readDisabledComponentsLPw(PackageSetting packageSetting, TypedXmlPullParser parser, int userId) throws IOException, XmlPullParserException { int outerDepth = parser.getDepth(); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8dc97566d1d2..8419a608dc41 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -933,8 +933,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { public void onWakeUp() { synchronized (mLock) { if (shouldEnableWakeGestureLp()) { - performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, false, - "Wake Up"); + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, "Wake Up"); mWindowWakeUpPolicy.wakeUpFromWakeGesture(); } } @@ -1403,7 +1402,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case LONG_PRESS_POWER_GLOBAL_ACTIONS: mPowerKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, "Power - Long Press - Global Actions"); showGlobalActions(); break; @@ -1415,14 +1414,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (ActivityManager.isUserAMonkey()) { break; } - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, "Power - Long Press - Shut Off"); sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF); break; case LONG_PRESS_POWER_GO_TO_VOICE_ASSIST: mPowerKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, "Power - Long Press - Go To Voice Assist"); // Some devices allow the voice assistant intent during setup (and use that intent // to launch something else, like Settings). So we explicitly allow that via the @@ -1431,7 +1430,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case LONG_PRESS_POWER_ASSISTANT: mPowerKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.ASSISTANT_BUTTON, false, + performHapticFeedback(HapticFeedbackConstants.ASSISTANT_BUTTON, "Power - Long Press - Go To Assistant"); final int powerKeyDeviceId = INVALID_INPUT_DEVICE_ID; launchAssistAction(null, powerKeyDeviceId, eventTime, @@ -1446,7 +1445,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case VERY_LONG_PRESS_POWER_GLOBAL_ACTIONS: mPowerKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, "Power - Very Long Press - Show Global Actions"); showGlobalActions(); break; @@ -1599,8 +1598,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY: mTalkbackShortcutController.toggleTalkback(mCurrentUserId); if (mTalkbackShortcutController.isTalkBackShortcutGestureEnabled()) { - performHapticFeedback(HapticFeedbackConstants.CONFIRM, /* always = */ - false, /* reason = */ + performHapticFeedback(HapticFeedbackConstants.CONFIRM, "Stem primary - Triple Press - Toggle Accessibility"); } break; @@ -1771,7 +1769,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override public void run() { mEndCallKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false, + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "End Call - Long Press - Show Global Actions"); showGlobalActionsInternal(); } @@ -2087,8 +2085,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return; } mHomeConsumed = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false, - "Home - Long Press"); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Home - Long Press"); switch (mLongPressOnHomeBehavior) { case LONG_PRESS_HOME_ALL_APPS: if (mHasFeatureLeanback) { @@ -2530,7 +2527,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS: performHapticFeedback( - HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false, + HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, "Power + Volume Up - Global Actions"); showGlobalActions(); mPowerKeyHandled = true; @@ -5078,8 +5075,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (useHapticFeedback) { - performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, false, - "Virtual Key - Press"); + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, "Virtual Key - Press"); } if (isWakeKey) { @@ -5971,8 +5967,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { public void setSafeMode(boolean safeMode) { mSafeMode = safeMode; if (safeMode) { - performHapticFeedback(HapticFeedbackConstants.SAFE_MODE_ENABLED, true, - "Safe Mode Enabled"); + performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), + HapticFeedbackConstants.SAFE_MODE_ENABLED, + "Safe Mode Enabled", HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + 0 /* privFlags */); } } @@ -6441,9 +6439,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { Settings.Global.THEATER_MODE_ON, 0) == 1; } - private boolean performHapticFeedback(int effectId, boolean always, String reason) { + private boolean performHapticFeedback(int effectId, String reason) { return performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), - effectId, always, reason, false /* fromIme */); + effectId, reason, 0 /* flags */, 0 /* privFlags */); } @Override @@ -6452,8 +6450,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } @Override - public boolean performHapticFeedback(int uid, String packageName, int effectId, - boolean always, String reason, boolean fromIme) { + public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason, + int flags, int privFlags) { if (!mVibrator.hasVibrator()) { return false; } @@ -6464,7 +6462,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } VibrationAttributes attrs = mHapticFeedbackVibrationProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ always, fromIme); + effectId, flags, privFlags); VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, effectId); mVibrator.vibrate(uid, packageName, effect, reason, attrs); return true; diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 6c05d70f8513..1b394f65c5eb 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -80,6 +80,7 @@ import android.os.RemoteException; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Display; +import android.view.HapticFeedbackConstants; import android.view.IDisplayFoldListener; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -1079,7 +1080,8 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * Call from application to perform haptic feedback on its window. */ public boolean performHapticFeedback(int uid, String packageName, int effectId, - boolean always, String reason, boolean fromIme); + String reason, @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags); /** * Called when we have started keeping the screen on because a window diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index ce0120c245d6..6fe1ccde7a4a 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -33,6 +33,7 @@ import static android.os.PowerManagerInternal.wakefulnessToString; import static com.android.internal.util.LatencyTracker.ACTION_TURN_ON_SCREEN; import static com.android.server.deviceidle.Flags.disableWakelocksInLightIdle; +import static com.android.server.display.DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG; import android.annotation.IntDef; import android.annotation.NonNull; @@ -239,9 +240,6 @@ public final class PowerManagerService extends SystemService // This should perhaps be a setting. private static final int SCREEN_BRIGHTNESS_BOOST_TIMEOUT = 5 * 1000; - // Float.NaN cannot be stored in config.xml so -2 is used instead - private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f; - // How long a partial wake lock must be held until we consider it a long wake lock. static final long MIN_LONG_WAKE_CHECK_INTERVAL = 60*1000; @@ -619,7 +617,6 @@ public final class PowerManagerService extends SystemService public final float mScreenBrightnessMinimum; public final float mScreenBrightnessMaximum; public final float mScreenBrightnessDefault; - public final float mScreenBrightnessDoze; public final float mScreenBrightnessDim; // Value we store for tracking face down behavior. @@ -1219,8 +1216,6 @@ public final class PowerManagerService extends SystemService .config_screenBrightnessSettingMaximumFloat); final float def = mContext.getResources().getFloat(com.android.internal.R.dimen .config_screenBrightnessSettingDefaultFloat); - final float doze = mContext.getResources().getFloat(com.android.internal.R.dimen - .config_screenBrightnessDozeFloat); final float dim = mContext.getResources().getFloat(com.android.internal.R.dimen .config_screenBrightnessDimFloat); @@ -1240,13 +1235,6 @@ public final class PowerManagerService extends SystemService mScreenBrightnessMaximum = max; mScreenBrightnessDefault = def; } - if (doze == INVALID_BRIGHTNESS_IN_CONFIG) { - mScreenBrightnessDoze = BrightnessSynchronizer.brightnessIntToFloat( - mContext.getResources().getInteger(com.android.internal.R.integer - .config_screenBrightnessDoze)); - } else { - mScreenBrightnessDoze = doze; - } if (dim == INVALID_BRIGHTNESS_IN_CONFIG) { mScreenBrightnessDim = BrightnessSynchronizer.brightnessIntToFloat( mContext.getResources().getInteger(com.android.internal.R.integer @@ -6090,8 +6078,6 @@ public final class PowerManagerService extends SystemService return mScreenBrightnessDefault; case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM: return mScreenBrightnessDim; - case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE: - return mScreenBrightnessDoze; default: return PowerManager.BRIGHTNESS_INVALID_FLOAT; } diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index dfccd1af3c50..bca81f52a1ac 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -1283,7 +1283,7 @@ public class StatsPullAtomService extends SystemService { } case FrameworkStatsLog.PROXY_BYTES_TRANSFER_BY_FG_BG: { final NetworkStats stats = getUidNetworkStatsSnapshotForTemplate( - new NetworkTemplate.Builder(MATCH_PROXY).build(), /*includeTags=*/true); + new NetworkTemplate.Builder(MATCH_PROXY).build(), /*includeTags=*/false); if (stats != null) { ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats), new int[]{TRANSPORT_BLUETOOTH}, diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java index 8138168f609e..98a2ba0d62cf 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java @@ -186,13 +186,13 @@ public final class HapticFeedbackVibrationProvider { * * @param effectId the haptic feedback effect ID whose respective vibration attributes we want * to get. - * @param bypassVibrationIntensitySetting {@code true} if the returned attribute should bypass - * vibration intensity settings. {@code false} otherwise. - * @param fromIme the haptic feedback is performed from an IME. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. * @return the {@link VibrationAttributes} that should be used for the provided haptic feedback. */ - public VibrationAttributes getVibrationAttributesForHapticFeedback( - int effectId, boolean bypassVibrationIntensitySetting, boolean fromIme) { + public VibrationAttributes getVibrationAttributesForHapticFeedback(int effectId, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { VibrationAttributes attrs; switch (effectId) { case HapticFeedbackConstants.EDGE_SQUEEZE: @@ -208,7 +208,7 @@ public final class HapticFeedbackVibrationProvider { break; case HapticFeedbackConstants.KEYBOARD_TAP: case HapticFeedbackConstants.KEYBOARD_RELEASE: - attrs = createKeyboardVibrationAttributes(fromIme); + attrs = createKeyboardVibrationAttributes(privFlags); break; case HapticFeedbackConstants.BIOMETRIC_CONFIRM: case HapticFeedbackConstants.BIOMETRIC_REJECT: @@ -218,18 +218,23 @@ public final class HapticFeedbackVibrationProvider { attrs = TOUCH_VIBRATION_ATTRIBUTES; } - int flags = 0; + int vibFlags = 0; + boolean fromIme = + (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0; + boolean bypassVibrationIntensitySetting = + (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0; if (bypassVibrationIntensitySetting) { - flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; + vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; } if (shouldBypassInterruptionPolicy(effectId)) { - flags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; + vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; } if (shouldBypassIntensityScale(effectId, fromIme)) { - flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; + vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; } - return flags == 0 ? attrs : new VibrationAttributes.Builder(attrs).setFlags(flags).build(); + return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs) + .setFlags(vibFlags).build(); } /** @@ -373,12 +378,16 @@ public final class HapticFeedbackVibrationProvider { return false; } - private VibrationAttributes createKeyboardVibrationAttributes(boolean fromIme) { - // Use touch attribute when the keyboard category is disable or it's not from an IME. - if (!Flags.keyboardCategoryEnabled() || !fromIme) { + private VibrationAttributes createKeyboardVibrationAttributes( + @HapticFeedbackConstants.PrivateFlags int privFlags) { + // Use touch attribute when the keyboard category is disable. + if (!Flags.keyboardCategoryEnabled()) { + return TOUCH_VIBRATION_ATTRIBUTES; + } + // Use touch attribute when the haptic is not apply to IME. + if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) { return TOUCH_VIBRATION_ATTRIBUTES; } - return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 5c15ccb55c65..4437a2ddf3a7 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -65,6 +65,7 @@ import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; +import android.view.HapticFeedbackConstants; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -439,13 +440,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override // Binder call public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant, - boolean always, String reason, boolean fromIme) { + String reason, int flags, int privFlags) { // Note that the `performHapticFeedback` method does not take a token argument from the // caller, and instead, uses this service as the token. This is to mitigate performance // impact that would otherwise be caused due to marshal latency. Haptic feedback effects are // short-lived, so we don't need to cancel when the process dies. - performHapticFeedbackInternal( - uid, deviceId, opPkg, constant, always, reason, /* token= */ this, fromIme); + performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */ + this, flags, privFlags); } /** @@ -456,8 +457,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @VisibleForTesting @Nullable HalVibration performHapticFeedbackInternal( - int uid, int deviceId, String opPkg, int constant, boolean always, String reason, - IBinder token, boolean fromIme) { + int uid, int deviceId, String opPkg, int constant, String reason, + IBinder token, int flags, int privFlags) { HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider(); if (hapticVibrationProvider == null) { Slog.e(TAG, "performHapticFeedback; haptic vibration provider not ready."); @@ -474,9 +475,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return null; } CombinedVibration vib = CombinedVibration.createParallel(effect); - VibrationAttributes attrs = - hapticVibrationProvider.getVibrationAttributesForHapticFeedback( - constant, /* bypassVibrationIntensitySetting= */ always, fromIme); + VibrationAttributes attrs = hapticVibrationProvider.getVibrationAttributesForHapticFeedback( + constant, flags, privFlags); reason = "performHapticFeedback(constant=" + constant + "): " + reason; VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, constant); return vibrateWithoutPermissionCheck(uid, deviceId, opPkg, vib, attrs, reason, token); @@ -2295,10 +2295,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { IBinder deathBinder = commonOptions.background ? VibratorManagerService.this : mShellCallbacksToken; + int flags = commonOptions.force + ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0; HalVibration vib = performHapticFeedbackInternal(Binder.getCallingUid(), Context.DEVICE_ID_DEFAULT, SHELL_PACKAGE_NAME, constant, - /* always= */ commonOptions.force, /* reason= */ commonOptions.description, - deathBinder, false /* fromIme */); + /* reason= */ commonOptions.description, deathBinder, flags, /* privFlags */ 0); maybeWaitOnVibration(vib, commonOptions); return 0; diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java index 68a417289c55..2755a80ea705 100644 --- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -26,13 +26,14 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.CameraCompatTaskInfo; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.ProtoLogGroup; import com.android.window.flags.Flags; /** @@ -56,6 +57,9 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa private boolean mIsCameraCompatTreatmentPending = false; + @Nullable + private Task mCameraTask; + CameraCompatFreeformPolicy(@NonNull DisplayContent displayContent, @NonNull CameraStateMonitor cameraStateMonitor, @NonNull ActivityRefresher activityRefresher) { @@ -116,6 +120,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa final int newCameraCompatMode = getCameraCompatMode(cameraActivity); if (newCameraCompatMode != existingCameraCompatMode) { mIsCameraCompatTreatmentPending = true; + mCameraTask = cameraActivity.getTask(); cameraActivity.mAppCompatController.getAppCompatCameraOverrides() .setFreeformCameraCompatMode(newCameraCompatMode); forceUpdateActivityAndTask(cameraActivity); @@ -127,18 +132,22 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa } @Override - public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, - @NonNull String cameraId) { - if (isActivityForCameraIdRefreshing(cameraId)) { - ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES, - "Display id=%d is notified that Camera %s is closed but activity is" - + " still refreshing. Rescheduling an update.", - mDisplayContent.mDisplayId, cameraId); - return false; + public boolean onCameraClosed(@NonNull String cameraId) { + // Top activity in the same task as the camera activity, or `null` if the task is + // closed. + final ActivityRecord topActivity = mCameraTask != null + ? mCameraTask.getTopActivity(/* isFinishing */ false, /* includeOverlays */ false) + : null; + if (topActivity != null) { + if (isActivityForCameraIdRefreshing(topActivity, cameraId)) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES, + "Display id=%d is notified that Camera %s is closed but activity is" + + " still refreshing. Rescheduling an update.", + mDisplayContent.mDisplayId, cameraId); + return false; + } } - cameraActivity.mAppCompatController.getAppCompatCameraOverrides() - .setFreeformCameraCompatMode(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE); - forceUpdateActivityAndTask(cameraActivity); + mCameraTask = null; mIsCameraCompatTreatmentPending = false; return true; } @@ -186,10 +195,9 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa && !activity.isEmbedded(); } - private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) { - final ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (topActivity == null || !isTreatmentEnabledForActivity(topActivity) + private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord topActivity, + @NonNull String cameraId) { + if (!isTreatmentEnabledForActivity(topActivity) || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) { return false; } diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java index a54141ce5230..068fc001ae2c 100644 --- a/services/core/java/com/android/server/wm/CameraStateMonitor.java +++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java @@ -61,9 +61,6 @@ class CameraStateMonitor { @NonNull private final Handler mHandler; - @Nullable - private ActivityRecord mCameraActivity; - // Bi-directional map between package names and active camera IDs since we need to 1) get a // camera id by a package name when resizing the window; 2) get a package name by a camera id // when camera connection is closed and we need to clean up our records. @@ -91,13 +88,13 @@ class CameraStateMonitor { @Override public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { synchronized (mWmService.mGlobalLock) { - notifyCameraOpened(cameraId, packageId); + notifyCameraOpenedWithDelay(cameraId, packageId); } } @Override public void onCameraClosed(@NonNull String cameraId) { synchronized (mWmService.mGlobalLock) { - notifyCameraClosed(cameraId); + notifyCameraClosedWithDelay(cameraId); } } }; @@ -131,8 +128,8 @@ class CameraStateMonitor { mCameraStateListeners.remove(listener); } - private void notifyCameraOpened( - @NonNull String cameraId, @NonNull String packageName) { + private void notifyCameraOpenedWithDelay(@NonNull String cameraId, + @NonNull String packageName) { // If an activity is restarting or camera is flipping, the camera connection can be // quickly closed and reopened. mScheduledToBeRemovedCameraIdSet.remove(cameraId); @@ -142,25 +139,30 @@ class CameraStateMonitor { // Some apps can’t handle configuration changes coming at the same time with Camera setup so // delaying orientation update to accommodate for that. mScheduledCompatModeUpdateCameraIdSet.add(cameraId); - mHandler.postDelayed( - () -> { - synchronized (mWmService.mGlobalLock) { - if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) { - // Camera compat mode update has happened already or was cancelled - // because camera was closed. - return; - } - mCameraIdPackageBiMapping.put(packageName, cameraId); - mCameraActivity = findCameraActivity(packageName); - if (mCameraActivity == null || mCameraActivity.getTask() == null) { - return; - } - notifyListenersCameraOpened(mCameraActivity, cameraId); - } - }, + mHandler.postDelayed(() -> notifyCameraOpenedInternal(cameraId, packageName), CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS); } + private void notifyCameraOpenedInternal(@NonNull String cameraId, @NonNull String packageName) { + synchronized (mWmService.mGlobalLock) { + if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) { + // Camera compat mode update has happened already or was cancelled + // because camera was closed. + return; + } + mCameraIdPackageBiMapping.put(packageName, cameraId); + // If there are multiple activities of the same package name and none of + // them are the top running activity, we do not apply treatment (rather than + // guessing and applying it to the wrong activity). + final ActivityRecord cameraActivity = + findUniqueActivityWithPackageName(packageName); + if (cameraActivity == null || cameraActivity.getTask() == null) { + return; + } + notifyListenersCameraOpened(cameraActivity, cameraId); + } + } + private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId) { for (int i = 0; i < mCameraStateListeners.size(); i++) { @@ -174,7 +176,13 @@ class CameraStateMonitor { } } - private void notifyCameraClosed(@NonNull String cameraId) { + /** + * Processes camera closed, and schedules notifying listeners. + * + * <p>The delay is introduced to avoid flickering when switching between front and back camera, + * and when an activity is refreshed due to camera compat treatment. + */ + private void notifyCameraClosedWithDelay(@NonNull String cameraId) { ProtoLog.v(WM_DEBUG_STATES, "Display id=%d is notified that Camera %s is closed.", mDisplayContent.mDisplayId, cameraId); @@ -217,9 +225,10 @@ class CameraStateMonitor { // Already reconnected to this camera, no need to clean up. return; } - if (mCameraActivity != null && mCurrentListenerForCameraActivity != null) { + + if (mCurrentListenerForCameraActivity != null) { boolean closeSuccessful = - mCurrentListenerForCameraActivity.onCameraClosed(mCameraActivity, cameraId); + mCurrentListenerForCameraActivity.onCameraClosed(cameraId); if (closeSuccessful) { mCameraIdPackageBiMapping.removeCameraId(cameraId); mCurrentListenerForCameraActivity = null; @@ -231,8 +240,14 @@ class CameraStateMonitor { } // TODO(b/335165310): verify that this works in multi instance and permission dialogs. + /** + * Finds a visible activity with the given package name. + * + * <p>If there are multiple visible activities with a given package name, and none of them are + * the `topRunningActivity`, returns null. + */ @Nullable - private ActivityRecord findCameraActivity(@NonNull String packageName) { + private ActivityRecord findUniqueActivityWithPackageName(@NonNull String packageName) { final ActivityRecord topActivity = mDisplayContent.topRunningActivity( /* considerKeyguardState= */ true); if (topActivity != null && topActivity.packageName.equals(packageName)) { @@ -277,11 +292,11 @@ class CameraStateMonitor { // TODO(b/336474959): try to decouple `cameraId` from the listeners. boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId); /** - * Notifies the compat listener that an activity has closed the camera. + * Notifies the compat listener that camera is closed. * * @return true if cleanup has been successful - the notifier might try again if false. */ // TODO(b/336474959): try to decouple `cameraId` from the listeners. - boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId); + boolean onCameraClosed(@NonNull String cameraId); } } diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index 50ac801a9d04..66653caaa73d 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -164,17 +164,16 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { private void calculateAndCentreInitialBounds(Task task, LaunchParamsController.LaunchParams outParams) { // TODO(b/319819547): Account for app constraints so apps do not become letterboxed - final Rect stableBounds = new Rect(); - task.getDisplayArea().getStableRect(stableBounds); + final Rect screenBounds = task.getDisplayArea().getBounds(); // The desired dimensions that a fully resizable window should take when initially entering // desktop mode. Calculated as a percentage of the available display area as defined by the // DESKTOP_MODE_INITIAL_BOUNDS_SCALE. - final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); outParams.mBounds.right = desiredWidth; outParams.mBounds.bottom = desiredHeight; - outParams.mBounds.offset(stableBounds.centerX() - outParams.mBounds.centerX(), - stableBounds.centerY() - outParams.mBounds.centerY()); + outParams.mBounds.offset(screenBounds.centerX() - outParams.mBounds.centerX(), + screenBounds.centerY() - outParams.mBounds.centerY()); } private void initLogBuilder(Task task, ActivityRecord activity) { diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 9998e1a016d0..1a0124a1d4de 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -342,12 +342,19 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp } @Override - public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, - @NonNull String cameraId) { + public boolean onCameraClosed(@NonNull String cameraId) { + // Top activity in the same task as the camera activity, or `null` if the task is + // closed. + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (topActivity == null) { + return true; + } + synchronized (this) { // TODO(b/336474959): Once refresh is implemented in `CameraCompatFreeformPolicy`, // consider checking this in CameraStateMonitor before notifying the listeners (this). - if (isActivityForCameraIdRefreshing(cameraId)) { + if (isActivityForCameraIdRefreshing(topActivity, cameraId)) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is notified that camera is closed but activity is" + " still refreshing. Rescheduling an update.", @@ -355,15 +362,15 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp return false; } } + ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is notified that Camera is closed, updating rotation.", mDisplayContent.mDisplayId); - final ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (topActivity == null - // Checking whether an activity in fullscreen rather than the task as this - // camera compat treatment doesn't cover activity embedding. - || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + // Checking whether an activity in fullscreen rather than the task as this camera compat + // treatment doesn't cover activity embedding. + // TODO(b/350495350): Consider checking whether this activity is the camera activity, or + // whether the top activity has the same task as the one which opened camera. + if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { return true; } recomputeConfigurationForCameraCompatIfNeeded(topActivity); @@ -372,14 +379,13 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp } // TODO(b/336474959): Do we need cameraId here? - private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) { - final ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (!isTreatmentEnabledForActivity(topActivity) - || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) { + private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord activity, + @NonNull String cameraId) { + if (!isTreatmentEnabledForActivity(activity) + || !mCameraStateMonitor.isCameraWithIdRunningForActivity(activity, cameraId)) { return false; } - return mActivityRefresher.isActivityRefreshing(topActivity); + return mActivityRefresher.isActivityRefreshing(activity); } private void recomputeConfigurationForCameraCompatIfNeeded( diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java index bf99ccdf1b0a..d79c11cecdc0 100644 --- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java +++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java @@ -408,9 +408,8 @@ public class ImmersiveModeConfirmation { final boolean intersectsTopCutout = topDisplayCutout.intersects( width - (windowWidth / 2), 0, width + (windowWidth / 2), topDisplayCutout.bottom); - if (mClingWindow != null && - (windowWidth < 0 || (width > 0 && intersectsTopCutout))) { - final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon); + if (windowWidth < 0 || (width > 0 && intersectsTopCutout)) { + final View iconView = findViewById(R.id.immersive_cling_icon); RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) iconView.getLayoutParams(); lp.topMargin = topDisplayCutout.bottom; diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index f5108f5bc93f..c26684f60731 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -324,19 +324,19 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) { + public boolean performHapticFeedback(int effectId, int flags, int privFlags) { final long ident = Binder.clearCallingIdentity(); try { - return mService.mPolicy.performHapticFeedback(mUid, mPackageName, - effectId, always, null, fromIme); + return mService.mPolicy.performHapticFeedback(mUid, mPackageName, effectId, null, flags, + privFlags); } finally { Binder.restoreCallingIdentity(ident); } } @Override - public void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme) { - performHapticFeedback(effectId, always, fromIme); + public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) { + performHapticFeedback(effectId, flags, privFlags); } /* Drag/drop */ diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2a3e94544aea..f6a68d58ea27 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.ActivityOptions.ANIM_CUSTOM; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -1897,7 +1898,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } - private void overrideAnimationOptionsToInfoIfNecessary(@NonNull TransitionInfo info) { + @VisibleForTesting + void overrideAnimationOptionsToInfoIfNecessary(@NonNull TransitionInfo info) { if (mOverrideOptions == null) { return; } @@ -1914,12 +1916,28 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { changes.get(i).setAnimationOptions(mOverrideOptions); // TODO(b/295805497): Extract mBackgroundColor from AnimationOptions. changes.get(i).setBackgroundColor(mOverrideOptions.getBackgroundColor()); + } else if (shouldApplyAnimOptionsToEmbeddedTf(container.asTaskFragment())) { + // We only override AnimationOptions because backgroundColor should be from + // TaskFragmentAnimationParams. + changes.get(i).setAnimationOptions(mOverrideOptions); } } } updateActivityTargetForCrossProfileAnimation(info); } + private boolean shouldApplyAnimOptionsToEmbeddedTf(@Nullable TaskFragment taskFragment) { + if (taskFragment == null || !taskFragment.isEmbedded()) { + return false; + } + if (taskFragment.getAnimationParams().hasOverrideAnimation()) { + // Always respect animation overrides from TaskFragmentAnimationParams. + return false; + } + // ActivityEmbedding animation adapter only support custom animation + return mOverrideOptions != null && mOverrideOptions.getType() == ANIM_CUSTOM; + } + /** * Updates activity open target if {@link #mOverrideOptions} is * {@link ANIM_OPEN_CROSS_PROFILE_APPS}. @@ -1929,8 +1947,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return; } for (int i = 0; i < mTargets.size(); ++i) { - final ActivityRecord activity = mTargets.get(i).mContainer - .asActivityRecord(); + final ActivityRecord activity = mTargets.get(i).mContainer.asActivityRecord(); final TransitionInfo.Change change = info.getChanges().get(i); if (activity == null || change.getMode() != TRANSIT_OPEN) { continue; @@ -2126,6 +2143,16 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } /** + + * Wallpaper will set itself as target if it wants to keep itself visible without a target. + */ + private static boolean wallpaperIsOwnTarget(WallpaperWindowToken wallpaper) { + final WindowState target = + wallpaper.getDisplayContent().mWallpaperController.getWallpaperTarget(); + return target != null && target.isDescendantOf(wallpaper); + } + + /** * Reset waitingToshow for all wallpapers, and commit the visibility of the visible ones */ private void commitVisibleWallpapers(SurfaceControl.Transaction t) { @@ -2133,8 +2160,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { for (int i = mParticipants.size() - 1; i >= 0; --i) { final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken(); if (wallpaper != null) { - if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) { + if (!wallpaper.isVisible() && (wallpaper.isVisibleRequested() + || (Flags.ensureWallpaperInTransitions() && showWallpaper))) { wallpaper.commitVisibility(showWallpaper); + } else if (Flags.ensureWallpaperInTransitions() && wallpaper.isVisible() + && !showWallpaper && !wallpaper.getDisplayContent().isKeyguardLocked() + && !wallpaperIsOwnTarget(wallpaper)) { + wallpaper.setVisibleRequested(false); } if (showWallpaper && Flags.ensureWallpaperInTransitions() && wallpaper.isVisibleRequested() @@ -2556,11 +2588,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (wc.asWindowState() != null) continue; final ChangeInfo changeInfo = changes.get(wc); - // Reject no-ops, unless wallpaper - if (!changeInfo.hasChanged() - && (!Flags.ensureWallpaperInTransitions() || wc.asWallpaperToken() == null)) { + // Reject no-ops + if (!changeInfo.hasChanged()) { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - " Rejecting as no-op: %s", wc); + " Rejecting as no-op: %s vis: %b", wc, wc.isVisibleRequested()); continue; } targets.add(changeInfo); diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index 3044abdffa8f..cdb14ab1dc0a 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -162,10 +162,6 @@ class TransparentPolicy { mTransparentPolicyState.clearInheritedCompatDisplayInsets(); } - TransparentPolicyState getTransparentPolicyState() { - return mTransparentPolicyState; - } - /** * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque * activity beneath using the given consumer and returns {@code true}. @@ -176,7 +172,7 @@ class TransparentPolicy { @NonNull Optional<ActivityRecord> getFirstOpaqueActivity() { - return isRunning() ? Optional.of(mTransparentPolicyState.mFirstOpaqueActivity) + return isRunning() ? Optional.ofNullable(mTransparentPolicyState.mFirstOpaqueActivity) : Optional.empty(); } @@ -216,10 +212,6 @@ class TransparentPolicy { SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; } - private void inheritConfiguration(ActivityRecord firstOpaque) { - mTransparentPolicyState.inheritFromOpaque(firstOpaque); - } - /** * Encapsulate the state for the current translucent activity when the transparent policy * has started. diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index ec7406ae6219..4231149336ec 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -179,6 +179,19 @@ <xs:element name="supportsVrr" type="xs:boolean" minOccurs="0"> <xs:annotation name="final"/> </xs:element> + <!-- Table that translates doze brightness sensor values to brightness values in + the float scale [0, 1]; -1 means the current brightness should be kept. + The following formula should be used for conversion between nits and the float + scale: float = (nits - minNits) / (maxNits - minNits). minNits and maxNits are + defined in screenBrightnessMap. --> + <xs:element type="float-array" name="dozeBrightnessSensorValueToBrightness"> + <xs:annotation name="final"/> + </xs:element> + <!-- The default screen brightness in the scale [0, 1] to use while the device is + dozing. --> + <xs:element type="nonNegativeDecimal" name="defaultDozeBrightness"> + <xs:annotation name="final"/> + </xs:element> </xs:sequence> </xs:complexType> </xs:element> @@ -859,6 +872,12 @@ </xs:sequence> </xs:complexType> + <xs:complexType name="float-array"> + <xs:sequence> + <xs:element name="item" type="nonNegativeDecimal" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="usiVersion"> <xs:element name="majorVersion" type="xs:nonNegativeInteger" minOccurs="1" maxOccurs="1"> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 68d74cf723e1..cec2787ca51f 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -125,9 +125,11 @@ package com.android.server.display.config { method public final java.math.BigInteger getAmbientLightHorizonLong(); method public final java.math.BigInteger getAmbientLightHorizonShort(); method public com.android.server.display.config.AutoBrightness getAutoBrightness(); + method public final java.math.BigDecimal getDefaultDozeBrightness(); method @Nullable public final com.android.server.display.config.DensityMapping getDensityMapping(); method @NonNull public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholds(); method public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholdsIdle(); + method public final com.android.server.display.config.FloatArray getDozeBrightnessSensorValueToBrightness(); method public final com.android.server.display.config.EvenDimmerMode getEvenDimmer(); method @Nullable public final com.android.server.display.config.HdrBrightnessConfig getHdrBrightnessConfig(); method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode(); @@ -163,9 +165,11 @@ package com.android.server.display.config { method public final void setAmbientLightHorizonLong(java.math.BigInteger); method public final void setAmbientLightHorizonShort(java.math.BigInteger); method public void setAutoBrightness(com.android.server.display.config.AutoBrightness); + method public final void setDefaultDozeBrightness(java.math.BigDecimal); method public final void setDensityMapping(@Nullable com.android.server.display.config.DensityMapping); method public final void setDisplayBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds); method public final void setDisplayBrightnessChangeThresholdsIdle(com.android.server.display.config.Thresholds); + method public final void setDozeBrightnessSensorValueToBrightness(com.android.server.display.config.FloatArray); method public final void setEvenDimmer(com.android.server.display.config.EvenDimmerMode); method public final void setHdrBrightnessConfig(@Nullable com.android.server.display.config.HdrBrightnessConfig); method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode); @@ -214,6 +218,11 @@ package com.android.server.display.config { method public void setTransitionPoint(java.math.BigDecimal); } + public class FloatArray { + ctor public FloatArray(); + method public java.util.List<java.math.BigDecimal> getItem(); + } + public class HbmTiming { ctor public HbmTiming(); method @NonNull public final java.math.BigInteger getTimeMaxSecs_all(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index e122fe039c45..032d6b56af1b 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1131,9 +1131,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public void onUserUnlocked(@NonNull TargetUser user) { - if (user.isPreCreated()) return; - mService.handleOnUserUnlocked(user.getUserIdentifier()); + public void onUserSwitching(@NonNull TargetUser from, @NonNull TargetUser to) { + if (to.isPreCreated()) return; + mService.handleOnUserSwitching(from.getUserIdentifier(), to.getUserIdentifier()); } } @@ -3831,8 +3831,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mDevicePolicyEngine.handleUnlockUser(userId); } - void handleOnUserUnlocked(int userId) { - showNewUserDisclaimerIfNecessary(userId); + void handleOnUserSwitching(int fromUserId, int toUserId) { + showNewUserDisclaimerIfNecessary(toUserId); } void handleStopUser(int userId) { diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 8f630af476c7..ce68b863d836 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -21,6 +21,7 @@ import static android.view.WindowInsets.Type.captionBar; import static com.android.compatibility.common.util.SystemUtil.eventually; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -33,18 +34,20 @@ import android.content.res.Configuration; import android.graphics.Insets; import android.os.RemoteException; import android.provider.Settings; -import android.support.test.uiautomator.By; -import android.support.test.uiautomator.UiDevice; -import android.support.test.uiautomator.UiObject2; -import android.support.test.uiautomator.Until; import android.util.Log; import android.view.WindowManagerGlobal; +import android.view.WindowManagerPolicyConstants; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; +import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper; import com.android.apps.inputmethod.simpleime.testing.TestActivity; @@ -66,6 +69,10 @@ public class InputMethodServiceTest { private static final String TAG = "SimpleIMSTest"; private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService"; private static final String EDIT_TEXT_DESC = "Input box"; + private static final String INPUT_METHOD_NAV_BACK_ID = + "android:id/input_method_nav_back"; + private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID = + "android:id/input_method_nav_ime_switcher"; private static final long TIMEOUT_IN_SECONDS = 3; private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1"; @@ -697,6 +704,151 @@ public class InputMethodServiceTest { assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); } + /** + * Verifies that clicking on the IME navigation bar back button hides the IME. + */ + @Test + public void testBackButtonClick() throws Exception { + boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService() + .hasNavigationBar(mInputMethodService.getDisplayId()); + assumeTrue("Must have a navigation bar", hasNavigationBar); + assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); + + setShowImeWithHardKeyboard(true /* enabled */); + + verifyInputViewStatusOnMainSync( + () -> { + // Ensure the IME navigation bar and the IME switch button are drawn. + mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged( + InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR + | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN + ); + assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(); + }, + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID); + backButtonUiObject.click(); + mInstrumentation.waitForIdleSync(); + + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } + + /** + * Verifies that long clicking on the IME navigation bar back button hides the IME. + */ + @Test + public void testBackButtonLongClick() throws Exception { + boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService() + .hasNavigationBar(mInputMethodService.getDisplayId()); + assumeTrue("Must have a navigation bar", hasNavigationBar); + assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); + + setShowImeWithHardKeyboard(true /* enabled */); + + verifyInputViewStatusOnMainSync( + () -> { + // Ensure the IME navigation bar and the IME switch button are drawn. + mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged( + InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR + | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN + ); + assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(); + }, + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID); + backButtonUiObject.longClick(); + mInstrumentation.waitForIdleSync(); + + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } + + /** + * Verifies that clicking on the IME switch button shows the Input Method Switcher Menu. + */ + @Test + public void testImeSwitchButtonClick() throws Exception { + boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService() + .hasNavigationBar(mInputMethodService.getDisplayId()); + assumeTrue("Must have a navigation bar", hasNavigationBar); + assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); + + setShowImeWithHardKeyboard(true /* enabled */); + + verifyInputViewStatusOnMainSync( + () -> { + // Ensure the IME navigation bar and the IME switch button are drawn. + mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged( + InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR + | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN + ); + assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(); + }, + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + final var imm = mContext.getSystemService(InputMethodManager.class); + + final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID); + imeSwitchButtonUiObject.click(); + mInstrumentation.waitForIdleSync(); + + assertWithMessage("Input Method Switcher Menu is shown") + .that(isInputMethodPickerShown(imm)) + .isTrue(); + + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + // Hide the Picker menu before finishing. + mUiDevice.pressBack(); + } + + /** + * Verifies that long clicking on the IME switch button shows the Input Method Switcher Menu. + */ + @Test + public void testImeSwitchButtonLongClick() throws Exception { + boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService() + .hasNavigationBar(mInputMethodService.getDisplayId()); + assumeTrue("Must have a navigation bar", hasNavigationBar); + assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); + + setShowImeWithHardKeyboard(true /* enabled */); + + verifyInputViewStatusOnMainSync( + () -> { + // Ensure the IME navigation bar and the IME switch button are drawn. + mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged( + InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR + | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN + ); + assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(); + }, + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + final var imm = mContext.getSystemService(InputMethodManager.class); + + final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID); + imeSwitchButtonUiObject.longClick(); + mInstrumentation.waitForIdleSync(); + + assertWithMessage("Input Method Switcher Menu is shown") + .that(isInputMethodPickerShown(imm)) + .isTrue(); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + + // Hide the Picker menu before finishing. + mUiDevice.pressBack(); + } + private void verifyInputViewStatus( Runnable runnable, boolean expected, boolean inputViewStarted) throws InterruptedException { @@ -844,6 +996,32 @@ public class InputMethodServiceTest { return SystemUtil.runShellCommandOrThrow(cmd); } + /** + * Checks if the Input Method Switcher Menu is shown. This runs by adopting the Shell's + * permission to ensure we have TEST_INPUT_METHOD permission. + */ + private static boolean isInputMethodPickerShown(@NonNull InputMethodManager imm) { + return SystemUtil.runWithShellPermissionIdentity(imm::isInputMethodPickerShown); + } + + @NonNull + private UiObject2 getUiObjectById(@NonNull String id) { + final var uiObject = mUiDevice.wait( + Until.findObject(By.res(id)), + TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS)); + assertThat(uiObject).isNotNull(); + return uiObject; + } + + /** + * Returns {@code true} if the navigation mode is gesture nav, and {@code false} otherwise. + */ + private boolean isGestureNavEnabled() { + return mContext.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + } + private void clickOnEditorText() { // Find the editText and click it. UiObject2 editTextUiObject = diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java index e81cf9df6660..dc0373239547 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java @@ -16,15 +16,26 @@ package com.android.server.inputmethod; +import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; +import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_RECENT; +import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_STATIC; +import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.SwitchMode; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; @@ -35,6 +46,7 @@ import androidx.annotation.Nullable; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; +import org.junit.Rule; import org.junit.Test; import java.util.ArrayList; @@ -51,6 +63,9 @@ public final class InputMethodSubtypeSwitchingControllerTest { private static final String SYSTEM_LOCALE = "en_US"; private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @NonNull private static InputMethodSubtype createTestSubtype(@NonNull String locale) { return new InputMethodSubtypeBuilder() @@ -170,7 +185,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { subtype = createTestSubtype(currentItem.mSubtypeName.toString()); } final ImeSubtypeListItem nextIme = controller.getNextInputMethod(onlyCurrentIme, - currentItem.mImi, subtype); + currentItem.mImi, subtype, MODE_STATIC, true /* forward */); assertEquals(nextItem, nextIme); } @@ -185,15 +200,16 @@ public final class InputMethodSubtypeSwitchingControllerTest { } } - private void onUserAction(@NonNull ControllerImpl controller, + private boolean onUserAction(@NonNull ControllerImpl controller, @NonNull ImeSubtypeListItem subtypeListItem) { InputMethodSubtype subtype = null; if (subtypeListItem.mSubtypeName != null) { subtype = createTestSubtype(subtypeListItem.mSubtypeName.toString()); } - controller.onUserActionLocked(subtypeListItem.mImi, subtype); + return controller.onUserActionLocked(subtypeListItem.mImi, subtype); } + @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP) @Test public void testControllerImpl() { final List<ImeSubtypeListItem> disabledItems = createDisabledImeSubtypes(); @@ -213,7 +229,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7); final ControllerImpl controller = ControllerImpl.createFrom( - null /* currentInstance */, enabledItems); + null /* currentInstance */, enabledItems, new ArrayList<>()); // switching-aware loop assertRotationOrder(controller, false /* onlyCurrentIme */, @@ -257,6 +273,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { disabledSubtypeUnawareIme, null); } + @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP) @Test public void testControllerImplWithUserAction() { final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes(); @@ -270,7 +287,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7); final ControllerImpl controller = ControllerImpl.createFrom( - null /* currentInstance */, enabledItems); + null /* currentInstance */, enabledItems, new ArrayList<>()); // === switching-aware loop === assertRotationOrder(controller, false /* onlyCurrentIme */, @@ -320,7 +337,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { // Rotation order should be preserved when created with the same subtype list. final List<ImeSubtypeListItem> sameEnabledItems = createEnabledImeSubtypes(); final ControllerImpl newController = ControllerImpl.createFrom(controller, - sameEnabledItems); + sameEnabledItems, new ArrayList<>()); assertRotationOrder(newController, false /* onlyCurrentIme */, subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp); assertRotationOrder(newController, false /* onlyCurrentIme */, @@ -332,7 +349,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk, switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme); final ControllerImpl anotherController = ControllerImpl.createFrom(controller, - differentEnabledItems); + differentEnabledItems, new ArrayList<>()); assertRotationOrder(anotherController, false /* onlyCurrentIme */, latinIme_en_us, latinIme_fr, subtypeAwareIme); assertRotationOrder(anotherController, false /* onlyCurrentIme */, @@ -370,6 +387,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { assertFalse(item_en_us_allcaps.mIsSystemLanguage); } + @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP) @SuppressWarnings("SelfComparison") @Test public void testImeSubtypeListComparator() { @@ -471,4 +489,739 @@ public final class InputMethodSubtypeSwitchingControllerTest { assertNotEquals(ime2, ime1); } } + + /** Verifies the static mode. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testModeStatic() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var english = items.get(0); + final var french = items.get(1); + final var italian = items.get(2); + final var simple = items.get(3); + final var latinIme = List.of(english, french, italian); + final var simpleIme = List.of(simple); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var hardwareEnglish = hardwareItems.get(0); + final var hardwareFrench = hardwareItems.get(1); + final var hardwareItalian = hardwareItems.get(2); + final var hardwareSimple = hardwareItems.get(3); + final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian); + final var hardwareSimpleIme = List.of(hardwareSimple); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + final int mode = MODE_STATIC; + + // Static mode matches the given items order. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // Set french IME as most recent. + assertTrue("Recency updated for french IME", onUserAction(controller, french)); + + // Static mode is not influenced by recency updates on non-hardware item. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertTrue("Recency updated for french hardware IME", + onUserAction(controller, hardwareFrench)); + + // Static mode is not influenced by recency updates on hardware item. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + } + + /** Verifies the recency mode. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testModeRecent() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var english = items.get(0); + final var french = items.get(1); + final var italian = items.get(2); + final var simple = items.get(3); + final var latinIme = List.of(english, french, italian); + final var simpleIme = List.of(simple); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var hardwareEnglish = hardwareItems.get(0); + final var hardwareFrench = hardwareItems.get(1); + final var hardwareItalian = hardwareItems.get(2); + final var hardwareSimple = hardwareItems.get(3); + final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian); + final var hardwareSimpleIme = List.of(hardwareSimple); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + final int mode = MODE_RECENT; + + // Recency order is initialized to static order. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertTrue("Recency updated for french IME", onUserAction(controller, french)); + final var recencyItems = List.of(french, english, italian, simple); + final var recencyLatinIme = List.of(french, english, italian); + final var recencySimpleIme = List.of(simple); + + // The order of non-hardware items is updated. + assertNextOrder(controller, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + // The order of hardware items remains unchanged for an action on a non-hardware item. + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertFalse("Recency not updated again for same IME", onUserAction(controller, french)); + + // The order of non-hardware items remains unchanged. + assertNextOrder(controller, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + // The order of hardware items remains unchanged. + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertTrue("Recency updated for french hardware IME", + onUserAction(controller, hardwareFrench)); + + final var recencyHardwareItems = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple); + final var recencyHardwareLatinIme = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian); + final var recencyHardwareSimpleIme = List.of(hardwareSimple); + + // The order of non-hardware items is unchanged. + assertNextOrder(controller, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + // The order of hardware items is updated. + assertNextOrder(controller, true /* forHardware */, mode, + recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme)); + } + + /** Verifies the auto mode. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testModeAuto() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var english = items.get(0); + final var french = items.get(1); + final var italian = items.get(2); + final var simple = items.get(3); + final var latinIme = List.of(english, french, italian); + final var simpleIme = List.of(simple); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var hardwareEnglish = hardwareItems.get(0); + final var hardwareFrench = hardwareItems.get(1); + final var hardwareItalian = hardwareItems.get(2); + final var hardwareSimple = hardwareItems.get(3); + final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian); + final var hardwareSimpleIme = List.of(hardwareSimple); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + final int mode = MODE_AUTO; + + // Auto mode resolves to static order initially. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // User action on french IME. + assertTrue("Recency updated for french IME", onUserAction(controller, french)); + + final var recencyItems = List.of(french, english, italian, simple); + final var recencyLatinIme = List.of(french, english, italian); + final var recencySimpleIme = List.of(simple); + + // Auto mode resolves to recency order for the first forward after user action, and to + // static order for the backwards direction. + assertNextOrder(controller, false /* forHardware */, mode, true /* forward */, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + assertNextOrder(controller, false /* forHardware */, mode, false /* forward */, + items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed())); + + // Auto mode resolves to recency order for the first forward after user action, + // but the recency was not updated for hardware items, so it's equivalent to static order. + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // Change IME, reset user action having happened. + controller.onInputMethodSubtypeChanged(); + + // Auto mode resolves to static order as there was no user action since changing IMEs. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // User action on french IME again. + assertFalse("Recency not updated again for same IME", onUserAction(controller, french)); + + // Auto mode still resolves to static order, as a user action on the currently most + // recent IME has no effect. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // User action on hardware french IME. + assertTrue("Recency updated for french hardware IME", + onUserAction(controller, hardwareFrench)); + + final var recencyHardware = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple); + final var recencyHardwareLatin = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian); + final var recencyHardwareSimple = List.of(hardwareSimple); + + // Auto mode resolves to recency order for the first forward direction after a user action + // on a hardware IME, and to static order for the backwards direction. + assertNextOrder(controller, false /* forHardware */, mode, true /* forward */, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + assertNextOrder(controller, false /* forHardware */, mode, false /* forward */, + items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed())); + + assertNextOrder(controller, true /* forHardware */, mode, true /* forward */, + recencyHardware, List.of(recencyHardwareLatin, recencyHardwareSimple)); + + assertNextOrder(controller, true /* forHardware */, mode, false /* forward */, + hardwareItems.reversed(), + List.of(hardwareLatinIme.reversed(), hardwareSimpleIme.reversed())); + } + + /** + * Verifies that the recency order is preserved only when updating with an equal list of items. + */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testUpdateList() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var english = items.get(0); + final var french = items.get(1); + final var italian = items.get(2); + final var simple = items.get(3); + + final var latinIme = List.of(english, french, italian); + final var simpleIme = List.of(simple); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme", + null, true /* supportsSwitchingToNextInputMethod */); + + final var hardwareEnglish = hardwareItems.get(0); + final var hardwareFrench = hardwareItems.get(1); + final var hardwareItalian = hardwareItems.get(2); + final var hardwareSimple = hardwareItems.get(3); + + final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian); + final var hardwareSimpleIme = List.of(hardwareSimple); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + final int mode = MODE_RECENT; + + // Recency order is initialized to static order. + assertNextOrder(controller, false /* forHardware */, mode, + items, List.of(latinIme, simpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + // User action on french IME. + assertTrue("Recency updated for french IME", onUserAction(controller, french)); + + final var equalItems = new ArrayList<>(items); + final var otherItems = new ArrayList<>(items); + otherItems.remove(simple); + + final var equalController = ControllerImpl.createFrom(controller, equalItems, + hardwareItems); + final var otherController = ControllerImpl.createFrom(controller, otherItems, + hardwareItems); + + final var recencyItems = List.of(french, english, italian, simple); + final var recencyLatinIme = List.of(french, english, italian); + final var recencySimpleIme = List.of(simple); + + assertNextOrder(controller, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + // The order of equal non-hardware items is unchanged. + assertNextOrder(equalController, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + // The order of other hardware items is reset. + assertNextOrder(otherController, false /* forHardware */, mode, + latinIme, List.of(latinIme)); + + // The order of hardware remains unchanged. + assertNextOrder(controller, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertNextOrder(equalController, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertNextOrder(otherController, true /* forHardware */, mode, + hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme)); + + assertTrue("Recency updated for french hardware IME", + onUserAction(controller, hardwareFrench)); + + final var equalHardwareItems = new ArrayList<>(hardwareItems); + final var otherHardwareItems = new ArrayList<>(hardwareItems); + otherHardwareItems.remove(hardwareSimple); + + final var equalHardwareController = ControllerImpl.createFrom(controller, items, + equalHardwareItems); + final var otherHardwareController = ControllerImpl.createFrom(controller, items, + otherHardwareItems); + + final var recencyHardwareItems = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple); + final var recencyHardwareLatinIme = + List.of(hardwareFrench, hardwareEnglish, hardwareItalian); + final var recencyHardwareSimpleIme = List.of(hardwareSimple); + + // The order of non-hardware items remains unchanged. + assertNextOrder(controller, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + assertNextOrder(equalHardwareController, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + assertNextOrder(otherHardwareController, false /* forHardware */, mode, + recencyItems, List.of(recencyLatinIme, recencySimpleIme)); + + assertNextOrder(controller, true /* forHardware */, mode, + recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme)); + + // The order of equal hardware items is unchanged. + assertNextOrder(equalHardwareController, true /* forHardware */, mode, + recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme)); + + // The order of other hardware items is reset. + assertNextOrder(otherHardwareController, true /* forHardware */, mode, + hardwareLatinIme, List.of(hardwareLatinIme)); + } + + /** Verifies that switch aware and switch unaware IMEs are combined together. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testSwitchAwareAndUnawareCombined() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "switchAware", "switchAware", + null, true /* supportsSwitchingToNextInputMethod*/); + addTestImeSubtypeListItems(items, "switchUnaware", "switchUnaware", + null, false /* supportsSwitchingToNextInputMethod*/); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchAware", "hardwareSwitchAware", + null, true /* supportsSwitchingToNextInputMethod*/); + addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchUnaware", "hardwareSwitchUnaware", + null, false /* supportsSwitchingToNextInputMethod*/); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) { + assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */, + mode, true /* forward */, items); + assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */, + mode, false /* forward */, items.reversed()); + + assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */, + mode, true /* forward */, hardwareItems); + assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */, + mode, false /* forward */, hardwareItems.reversed()); + } + } + + /** Verifies that an empty controller can't take any actions. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testEmptyList() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(), + List.of()); + + assertNoAction(controller, false /* forHardware */, items); + assertNoAction(controller, true /* forHardware */, hardwareItems); + } + + /** Verifies that a controller with a single item can't take any actions. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testSingleItemList() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, + List.of(items.get(0)), List.of(hardwareItems.get(0))); + + assertNoAction(controller, false /* forHardware */, items); + assertNoAction(controller, true /* forHardware */, hardwareItems); + } + + /** Verifies that a controller can't take any actions for unknown items. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testUnknownItems() { + final var items = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + final var unknownItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme", + List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + + final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, + hardwareItems); + + assertNoAction(controller, false /* forHardware */, unknownItems); + assertNoAction(controller, true /* forHardware */, unknownHardwareItems); + } + + /** Verifies that the IME name does influence the comparison order. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareImeName() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var imeX = createTestItem(component, "ImeX", "A", "en_US", 0); + final var imeY = createTestItem(component, "ImeY", "A", "en_US", 0); + + assertTrue("Smaller IME name should be smaller.", imeX.compareTo(imeY) < 0); + assertTrue("Larger IME name should be larger.", imeY.compareTo(imeX) > 0); + } + + /** Verifies that the IME ID does influence the comparison order. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareImeId() { + final var component1 = new ComponentName("com.example.ime1", "Ime"); + final var component2 = new ComponentName("com.example.ime2", "Ime"); + final var ime1 = createTestItem(component1, "Ime", "A", "en_US", 0); + final var ime2 = createTestItem(component2, "Ime", "A", "en_US", 0); + + assertTrue("Smaller IME ID should be smaller.", ime1.compareTo(ime2) < 0); + assertTrue("Larger IME ID should be larger.", ime2.compareTo(ime1) > 0); + } + + /** Verifies that comparison on self returns an equal order. */ + @SuppressWarnings("SelfComparison") + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareSelf() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var item = createTestItem(component, "Ime", "A", "en_US", 0); + + assertEquals("Item should have the same order to itself.", 0, item.compareTo(item)); + } + + /** Verifies that comparison on an equivalent item returns an equal order. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareEquivalent() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var item = createTestItem(component, "Ime", "A", "en_US", 0); + final var equivalent = createTestItem(component, "Ime", "A", "en_US", 0); + + assertEquals("Equivalent items should have the same order.", 0, item.compareTo(equivalent)); + } + + /** + * Verifies that the system locale and system language do not the influence comparison order. + */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareSystemLocaleSystemLanguage() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var japanese = createTestItem(component, "Ime", "A", "ja_JP", 0); + final var systemLanguage = createTestItem(component, "Ime", "A", "en_GB", 0); + final var systemLocale = createTestItem(component, "Ime", "A", "en_US", 0); + + assertFalse(japanese.mIsSystemLanguage); + assertFalse(japanese.mIsSystemLocale); + assertTrue(systemLanguage.mIsSystemLanguage); + assertFalse(systemLanguage.mIsSystemLocale); + assertTrue(systemLocale.mIsSystemLanguage); + assertTrue(systemLocale.mIsSystemLocale); + + assertEquals("System language shouldn't influence comparison over non-system language.", + 0, japanese.compareTo(systemLanguage)); + assertEquals("System locale shouldn't influence comparison over non-system locale.", + 0, japanese.compareTo(systemLocale)); + assertEquals("System locale shouldn't influence comparison over system language.", + 0, systemLanguage.compareTo(systemLocale)); + } + + /** Verifies that the subtype name does not influence the comparison order. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareSubtypeName() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var subtypeA = createTestItem(component, "Ime", "A", "en_US", 0); + final var subtypeB = createTestItem(component, "Ime", "B", "en_US", 0); + + assertEquals("Subtype name shouldn't influence comparison.", + 0, subtypeA.compareTo(subtypeB)); + } + + /** Verifies that the subtype index does not influence the comparison order. */ + @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) + @Test + public void testCompareSubtypeIndex() { + final var component = new ComponentName("com.example.ime", "Ime"); + final var subtype0 = createTestItem(component, "Ime1", "A", "en_US", 0); + final var subtype1 = createTestItem(component, "Ime1", "A", "en_US", 1); + + assertEquals("Subtype index shouldn't influence comparison.", + 0, subtype0.compareTo(subtype1)); + } + + /** + * Verifies that the controller's next item order matches the given one, and cycles back at + * the end, both across all IMEs, and also per each IME. If a single item is given, verifies + * that no next item is returned. + * + * @param controller the controller to use for finding the next items. + * @param forHardware whether to find the next hardware item, or software item. + * @param mode the switching mode. + * @param forward whether to search forwards or backwards in the list. + * @param allItems the list of items across all IMEs. + * @param perImeItems the list of lists of items per IME. + */ + private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware, + @SwitchMode int mode, boolean forward, @NonNull List<ImeSubtypeListItem> allItems, + @NonNull List<List<ImeSubtypeListItem>> perImeItems) { + assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode, + forward, allItems); + + for (var imeItems : perImeItems) { + assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode, + forward, imeItems); + } + } + + /** + * Verifies that the controller's next item order matches the given one, and cycles back at + * the end, both across all IMEs, and also per each IME. This checks the forward direction + * with the given items, and the backwards order with the items reversed. If a single item is + * given, verifies that no next item is returned. + * + * @param controller the controller to use for finding the next items. + * @param forHardware whether to find the next hardware item, or software item. + * @param mode the switching mode. + * @param allItems the list of items across all IMEs. + * @param perImeItems the list of lists of items per IME. + */ + private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware, + @SwitchMode int mode, @NonNull List<ImeSubtypeListItem> allItems, + @NonNull List<List<ImeSubtypeListItem>> perImeItems) { + assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode, + true /* forward */, allItems); + assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode, + false /* forward */, allItems.reversed()); + + for (var imeItems : perImeItems) { + assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode, + true /* forward */, imeItems); + assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode, + false /* forward */, imeItems.reversed()); + } + } + + /** + * Verifies that the controller's next item order (starting from the first one in {@code items} + * matches the given on, and cycles back at the end. If a single item is given, verifies that + * no next item is returned. + * + * @param controller the controller to use for finding the next items. + * @param forHardware whether to find the next hardware item, or software item. + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param mode the switching mode. + * @param forward whether to search forwards or backwards in the list. + * @param items the list of items to verify, in the expected order. + */ + private static void assertNextOrder(@NonNull ControllerImpl controller, + boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward, + @NonNull List<ImeSubtypeListItem> items) { + final int numItems = items.size(); + if (numItems == 0) { + return; + } else if (numItems == 1) { + // Single item controllers should never return a next item. + assertNextItem(controller, forHardware, onlyCurrentIme, mode, forward, items.get(0), + null /* expectedNext*/); + return; + } + + var item = items.get(0); + + final var expectedNextItems = new ArrayList<>(items); + // Add first item in the last position of expected order, to ensure the order is cyclic. + expectedNextItems.add(item); + + final var nextItems = new ArrayList<>(); + // Add first item in the first position of actual order, to ensure the order is cyclic. + nextItems.add(item); + + // Compute the nextItems starting from the first given item, and compare the order. + for (int i = 0; i < numItems; i++) { + item = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward, item); + assertNotNull("Next item shouldn't be null.", item); + nextItems.add(item); + } + + assertEquals("Rotation order doesn't match.", expectedNextItems, nextItems); + } + + /** + * Verifies that the controller gets the expected next value from the given item. + * + * @param controller the controller to sue for finding the next value. + * @param forHardware whether to find the next hardware item, or software item. + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param mode the switching mode. + * @param forward whether to search forwards or backwards in the list. + * @param item the item to find the next value from. + * @param expectedNext the expected next value. + */ + private static void assertNextItem(@NonNull ControllerImpl controller, + boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward, + @NonNull ImeSubtypeListItem item, @Nullable ImeSubtypeListItem expectedNext) { + final var nextItem = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward, + item); + assertEquals("Next item doesn't match.", expectedNext, nextItem); + } + + /** + * Gets the next value from the given item. + * + * @param controller the controller to use for finding the next value. + * @param forHardware whether to find the next hardware item, or software item. + * @param onlyCurrentIme whether to consider only subtypes of the current input method. + * @param mode the switching mode. + * @param forward whether to search forwards or backwards in the list. + * @param item the item to find the next value from. + * @return the next item found, otherwise {@code null}. + */ + @Nullable + private static ImeSubtypeListItem getNextItem(@NonNull ControllerImpl controller, + boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward, + @NonNull ImeSubtypeListItem item) { + final var subtype = item.mSubtypeName != null + ? createTestSubtype(item.mSubtypeName.toString()) : null; + return forHardware + ? controller.getNextInputMethodForHardware( + onlyCurrentIme, item.mImi, subtype, mode, forward) + : controller.getNextInputMethod( + onlyCurrentIme, item.mImi, subtype, mode, forward); + } + + /** + * Verifies that no next items can be found, and the recency cannot be updated for the + * given items. + * + * @param controller the controller to verify the items on. + * @param forHardware whether to try finding the next hardware item, or software item. + * @param items the list of items to verify. + */ + private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware, + @NonNull List<ImeSubtypeListItem> items) { + for (var item : items) { + for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) { + assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode, + false /* forward */, item, null /* expectedNext */); + assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode, + true /* forward */, item, null /* expectedNext */); + assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode, + false /* forward */, item, null /* expectedNext */); + assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode, + true /* forward */, item, null /* expectedNext */); + } + + assertFalse("User action shouldn't have updated the recency.", + onUserAction(controller, item)); + } + } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java index 81fb1a092887..d59f28b4cad2 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java @@ -98,8 +98,8 @@ public final class UserDataRepositoryTest { assertThat(allUserData.get(0).mBindingController.getUserId()).isEqualTo(ANY_USER_ID); } - private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) { - final var collected = new ArrayList<UserDataRepository.UserData>(); + private List<UserData> collectUserData(UserDataRepository repository) { + final var collected = new ArrayList<UserData>(); repository.forAllUserData(userData -> collected.add(userData)); return collected; } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index 71383069d08a..dec463444faa 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -1046,6 +1046,77 @@ public class PackageManagerSettingsTests { } @Test + public void testWriteReadBaseRevisionCode() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed() + .setUid(packageSetting.getAppId()) + .hideAsFinal()); + + final int revisionCode = 311; + packageSetting.setBaseRevisionCode(revisionCode); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + assertThat(settings.getPackageLPr(PACKAGE_NAME_1).getBaseRevisionCode(), is(revisionCode)); + } + + @Test + public void testHasPkg_writeReadSplitVersions() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed() + .setUid(packageSetting.getAppId()) + .hideAsFinal()); + + final String splitOne = "one"; + final String splitTwo = "two"; + final int revisionOne = 311; + final int revisionTwo = 330; + packageSetting.setSplitNames(new String[] { splitOne, splitTwo}); + packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionTwo}); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + assertThat(resultSetting.getSplitNames().length, is(0)); + assertThat(resultSetting.getSplitRevisionCodes().length, is(0)); + } + + @Test + public void testNoPkg_writeReadSplitVersions() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + + final String splitOne = "one"; + final String splitTwo = "two"; + final int revisionOne = 311; + final int revisionTwo = 330; + packageSetting.setSplitNames(new String[] { splitOne, splitTwo}); + packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionTwo}); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + assertThat(resultSetting.getSplitNames()[0], is(splitOne)); + assertThat(resultSetting.getSplitNames()[1], is(splitTwo)); + assertThat(resultSetting.getSplitRevisionCodes()[0], is(revisionOne)); + assertThat(resultSetting.getSplitRevisionCodes()[1], is(revisionTwo)); + } + + @Test public void testWriteReadArchiveState() { Settings settings = makeSettings(); PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java index 2d4a29b3ed6f..f690b1bbfccf 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java @@ -18,8 +18,6 @@ package com.android.server.display; import static org.junit.Assert.assertEquals; -import android.hardware.display.BrightnessInfo; - import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -114,10 +112,7 @@ public class DisplayBrightnessStateTest { .append("\n mBrightnessAdjustmentFlag:") .append(displayBrightnessState.getBrightnessAdjustmentFlag()) .append("\n mIsUserInitiatedChange:") - .append(displayBrightnessState.isUserInitiatedChange()) - .append("\n mBrightnessMaxReason:") - .append(BrightnessInfo.briMaxReasonToString( - displayBrightnessState.getBrightnessMaxReason())); + .append(displayBrightnessState.isUserInitiatedChange()); return sb.toString(); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 343792336247..d4506831d9c2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -965,6 +965,51 @@ public final class DisplayDeviceConfigTest { assertThat(supportedModeData.vsyncRate).isEqualTo(240); } + @Test + public void testDozeBrightness_Ddc() throws IOException { + when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true); + setupDisplayDeviceConfigFromDisplayConfigFile(); + + assertArrayEquals(new float[]{ -1, 0.1f, 0.2f, 0.3f, 0.4f }, + mDisplayDeviceConfig.getDozeBrightnessSensorValueToBrightness(), SMALL_DELTA); + assertEquals(0.25f, mDisplayDeviceConfig.getDefaultDozeBrightness(), SMALL_DELTA); + } + + @Test + public void testDefaultDozeBrightness_FallBackToConfigXmlFloat() throws IOException { + setupDisplayDeviceConfigFromConfigResourceFile(); + when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true); + when(mResources.getFloat(com.android.internal.R.dimen.config_screenBrightnessDozeFloat)) + .thenReturn(0.31f); + when(mResources.getInteger(com.android.internal.R.integer.config_screenBrightnessDoze)) + .thenReturn(90); + + // Empty display config file + setupDisplayDeviceConfigFromDisplayConfigFile( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<displayConfiguration />\n"); + + assertEquals(0.31f, mDisplayDeviceConfig.getDefaultDozeBrightness(), ZERO_DELTA); + } + + @Test + public void testDefaultDozeBrightness_FallBackToConfigXmlInt() throws IOException { + setupDisplayDeviceConfigFromConfigResourceFile(); + when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true); + when(mResources.getFloat(com.android.internal.R.dimen.config_screenBrightnessDozeFloat)) + .thenReturn(DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG); + when(mResources.getInteger(com.android.internal.R.integer.config_screenBrightnessDoze)) + .thenReturn(90); + + // Empty display config file + setupDisplayDeviceConfigFromDisplayConfigFile( + "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<displayConfiguration />\n"); + + assertEquals(brightnessIntToFloat(90), + mDisplayDeviceConfig.getDefaultDozeBrightness(), ZERO_DELTA); + } + private String getValidLuxThrottling() { return "<luxThrottling>\n" + " <brightnessLimitMap>\n" @@ -1708,6 +1753,16 @@ public final class DisplayDeviceConfigTest { + "</point>" + "</luxThresholds>" + "</idleScreenRefreshRateTimeout>" + + "<dozeBrightnessSensorValueToBrightness>\n" + + "<item>-1</item>\n" + + "<item>0.1</item>\n" + + "<item>0.2</item>\n" + + "<item>0.3</item>\n" + + "<item>0.4</item>\n" + + "</dozeBrightnessSensorValueToBrightness>\n" + + "<defaultDozeBrightness>" + + "0.25" + + "</defaultDozeBrightness>\n" + "</displayConfiguration>\n"; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 5c291569b8d8..624c8971d36f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -117,6 +117,7 @@ public final class DisplayPowerControllerTest { private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789"; private static final float PROX_SENSOR_MAX_RANGE = 5; private static final float DOZE_SCALE_FACTOR = 0.34f; + private static final float DEFAULT_DOZE_BRIGHTNESS = 0.121f; private static final float BRIGHTNESS_RAMP_RATE_MINIMUM = 0.0f; private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f; @@ -2051,9 +2052,6 @@ public final class DisplayPowerControllerTest { @Test public void testDefaultDozeBrightness() { - float brightness = 0.121f; - when(mPowerManagerMock.getBrightnessConstraint( - PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness); mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); @@ -2069,15 +2067,25 @@ public final class DisplayPowerControllerTest { mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); advanceTime(1); // Run updatePowerState - verify(mHolder.animator).animateTo(eq(brightness), /* linearSecondTarget= */ anyFloat(), - eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false)); + verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS), + /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), + eq(false)); + + // The display device changes and the default doze brightness changes + setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class), + mHolder.config, /* isEnabled= */ true); + when(mHolder.config.getDefaultDozeBrightness()).thenReturn(DEFAULT_DOZE_BRIGHTNESS / 2); + mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY); + + advanceTime(1); // Run updatePowerState + + verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS / 2), + /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), + eq(false)); } @Test public void testDefaultDozeBrightness_ShouldNotBeUsedIfAutoBrightnessAllowedInDoze() { - float brightness = 0.121f; - when(mPowerManagerMock.getBrightnessConstraint( - PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness); mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); @@ -2093,7 +2101,7 @@ public final class DisplayPowerControllerTest { mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); advanceTime(1); // Run updatePowerState - verify(mHolder.animator, never()).animateTo(eq(brightness), + verify(mHolder.animator, never()).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS), /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(), /* ignoreAnimationLimits= */ anyBoolean()); } @@ -2151,6 +2159,8 @@ public final class DisplayPowerControllerTest { new SensorData(Sensor.STRING_TYPE_LIGHT, null)); when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux()) .thenReturn(new int[0]); + when(displayDeviceConfigMock.getDefaultDozeBrightness()) + .thenReturn(DEFAULT_DOZE_BRIGHTNESS); when(displayDeviceConfigMock.getBrightnessRampFastDecrease()) .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index 93dfbcb7f07f..e982153acbd1 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.SensorManager; +import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.PowerManager; @@ -154,6 +155,12 @@ public class BrightnessClamperControllerTest { } @Test + public void testMaxReasonIsNoneOnInit() { + assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, + mClamperController.getBrightnessMaxReason()); + } + + @Test public void testOnDisplayChanged_DelegatesToClamper() { mClamperController.onDisplayChanged(mMockDisplayDeviceData); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 753db125ffe3..b9e99dd2e1e4 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -36,6 +36,8 @@ android_test { "-Werror", ], static_libs: [ + "a11ychecker-protos-java-proto-lite", + "aatf", "cts-input-lib", "frameworks-base-testutils", "services.accessibility", diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java new file mode 100644 index 000000000000..90d427596ab2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 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.server.accessibility.a11ychecker; + +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_CLASS_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_VERSION_CODE; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_VERSION_CODE; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE; +import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.test.runner.AndroidJUnit4; + +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck; +import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@RunWith(AndroidJUnit4.class) +public class AccessibilityCheckerUtilsTest { + + PackageManager mMockPackageManager; + + @Before + public void setUp() throws PackageManager.NameNotFoundException { + mMockPackageManager = getMockPackageManagerWithInstalledApps(); + } + + @Test + public void processResults_happyPath_setsAllFields() { + AccessibilityNodeInfo mockNodeInfo = + new MockAccessibilityNodeInfoBuilder() + .setViewIdResourceName("TargetNode") + .build(); + AccessibilityHierarchyCheckResult result1 = + new AccessibilityHierarchyCheckResult( + SpeakableTextPresentCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1, + null); + AccessibilityHierarchyCheckResult result2 = + new AccessibilityHierarchyCheckResult( + TouchTargetSizeCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null); + AccessibilityHierarchyCheckResult result3 = + new AccessibilityHierarchyCheckResult( + ClassNameCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 5, null); + AccessibilityHierarchyCheckResult result4 = + new AccessibilityHierarchyCheckResult( + ClickableSpanCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5, + null); + + Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms = + AccessibilityCheckerUtils.processResults( + mockNodeInfo, + List.of(result1, result2, result3, result4), + null, + mMockPackageManager, + new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, + TEST_A11Y_SERVICE_CLASS_NAME)); + + assertThat(atoms).containsExactly( + createAtom(A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK, + A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1), + createAtom(A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, + A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2) + ); + } + + @Test + public void processResults_packageNameNotFound_returnsEmptySet() + throws PackageManager.NameNotFoundException { + when(mMockPackageManager.getPackageInfo("com.uninstalled.app", 0)) + .thenThrow(PackageManager.NameNotFoundException.class); + AccessibilityNodeInfo mockNodeInfo = + new MockAccessibilityNodeInfoBuilder() + .setPackageName("com.uninstalled.app") + .setViewIdResourceName("TargetNode") + .build(); + AccessibilityHierarchyCheckResult result1 = + new AccessibilityHierarchyCheckResult( + TouchTargetSizeCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1, + null); + AccessibilityHierarchyCheckResult result2 = + new AccessibilityHierarchyCheckResult( + TouchTargetSizeCheck.class, + AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null); + + Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms = + AccessibilityCheckerUtils.processResults( + mockNodeInfo, + List.of(result1, result2), + null, + mMockPackageManager, + new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, + TEST_A11Y_SERVICE_CLASS_NAME)); + + assertThat(atoms).isEmpty(); + } + + @Test + public void getActivityName_hasWindowStateChangedEvent_returnsActivityName() { + AccessibilityEvent accessibilityEvent = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + accessibilityEvent.setPackageName(TEST_APP_PACKAGE_NAME); + accessibilityEvent.setClassName(TEST_ACTIVITY_NAME); + + assertThat(AccessibilityCheckerUtils.getActivityName(mMockPackageManager, + accessibilityEvent)).isEqualTo("MainActivity"); + } + + // Makes sure the AccessibilityHierarchyCheck class to enum mapping is up to date with the + // latest prod preset. + @Test + public void checkClassToEnumMap_hasAllLatestPreset() { + ImmutableSet<AccessibilityHierarchyCheck> checkPreset = + AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( + AccessibilityCheckPreset.LATEST); + Set<Class<? extends AccessibilityHierarchyCheck>> latestCheckClasses = + checkPreset.stream().map(AccessibilityHierarchyCheck::getClass).collect( + Collectors.toUnmodifiableSet()); + + assertThat(AccessibilityCheckerUtils.CHECK_CLASS_TO_ENUM_MAP.keySet()) + .containsExactlyElementsIn(latestCheckClasses); + } + + + private static A11yCheckerProto.AccessibilityCheckResultReported createAtom( + A11yCheckerProto.AccessibilityCheckClass checkClass, + A11yCheckerProto.AccessibilityCheckResultType resultType, + int resultId) { + return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder() + .setPackageName(TEST_APP_PACKAGE_NAME) + .setAppVersionCode(TEST_APP_VERSION_CODE) + .setUiElementPath(TEST_APP_PACKAGE_NAME + ":TargetNode") + .setWindowTitle(TEST_WINDOW_TITLE) + .setActivityName("") + .setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, + TEST_A11Y_SERVICE_CLASS_NAME).flattenToString()) + .setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE) + .setResultCheckClass(checkClass) + .setResultType(resultType) + .setResultId(resultId) + .build(); + } + +} diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java index 438277b272cb..a53f42ece21f 100644 --- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package android.view.accessibility.a11ychecker; +package com.android.server.accessibility.a11ychecker; -import static android.view.accessibility.a11ychecker.MockAccessibilityNodeInfoBuilder.PACKAGE_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME; import static com.google.common.truth.Truth.assertThat; @@ -36,7 +36,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AccessibilityNodePathBuilderTest { - public static final String RESOURCE_ID_PREFIX = PACKAGE_NAME + ":id/"; + public static final String RESOURCE_ID_PREFIX = TEST_APP_PACKAGE_NAME + ":id/"; @Test public void createNodePath_pathWithResourceNames() { @@ -55,11 +55,11 @@ public class AccessibilityNodePathBuilderTest { .build(); assertThat(AccessibilityNodePathBuilder.createNodePath(child)) - .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(parent)) - .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(root)) - .isEqualTo(PACKAGE_NAME + ":root_node"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node"); } @Test @@ -81,11 +81,11 @@ public class AccessibilityNodePathBuilderTest { .build(); assertThat(AccessibilityNodePathBuilder.createNodePath(child)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(parent)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(root)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout"); } @Test @@ -105,11 +105,11 @@ public class AccessibilityNodePathBuilderTest { .build(); assertThat(AccessibilityNodePathBuilder.createNodePath(child1)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout/child1[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/child1[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(child2)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout/TextView[2]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/TextView[2]"); assertThat(AccessibilityNodePathBuilder.createNodePath(parent)) - .isEqualTo(PACKAGE_NAME + ":FrameLayout"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout"); } @Test @@ -133,13 +133,13 @@ public class AccessibilityNodePathBuilderTest { .build(); assertThat(AccessibilityNodePathBuilder.createNodePath(child1)) - .isEqualTo(PACKAGE_NAME + ":parentId/childId[1]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childId[1]"); assertThat(AccessibilityNodePathBuilder.createNodePath(child2)) - .isEqualTo(PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]"); assertThat(AccessibilityNodePathBuilder.createNodePath(child3)) - .isEqualTo(PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]"); assertThat(AccessibilityNodePathBuilder.createNodePath(parent)) - .isEqualTo(PACKAGE_NAME + ":parentId"); + .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId"); } } diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java index e363f0c81720..7cd3535e72ba 100644 --- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java @@ -14,21 +14,33 @@ * limitations under the License. */ -package android.view.accessibility.a11ychecker; +package com.android.server.accessibility.a11ychecker; + +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME; +import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; import java.util.List; final class MockAccessibilityNodeInfoBuilder { - static final String PACKAGE_NAME = "com.example.app"; private final AccessibilityNodeInfo mMockNodeInfo = mock(AccessibilityNodeInfo.class); MockAccessibilityNodeInfoBuilder() { - when(mMockNodeInfo.getPackageName()).thenReturn(PACKAGE_NAME); + setPackageName(TEST_APP_PACKAGE_NAME); + + AccessibilityWindowInfo windowInfo = new AccessibilityWindowInfo(); + windowInfo.setTitle(TEST_WINDOW_TITLE); + when(mMockNodeInfo.getWindow()).thenReturn(windowInfo); + } + + MockAccessibilityNodeInfoBuilder setPackageName(String packageName) { + when(mMockNodeInfo.getPackageName()).thenReturn(packageName); + return this; } MockAccessibilityNodeInfoBuilder setClassName(String className) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS new file mode 100644 index 000000000000..7bdc0297907c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS @@ -0,0 +1 @@ +include /services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java new file mode 100644 index 000000000000..a04bbee05730 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright 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.server.accessibility.a11ychecker; + +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import org.mockito.Mockito; + +public class TestUtils { + static final String TEST_APP_PACKAGE_NAME = "com.example.app"; + static final int TEST_APP_VERSION_CODE = 12321; + static final String TEST_ACTIVITY_NAME = "com.example.app.MainActivity"; + static final String TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME = "com.assistive.app"; + static final String TEST_A11Y_SERVICE_CLASS_NAME = "MyA11yService"; + static final int TEST_A11Y_SERVICE_SOURCE_VERSION_CODE = 333555; + static final String TEST_WINDOW_TITLE = "Example window"; + + static PackageManager getMockPackageManagerWithInstalledApps() + throws PackageManager.NameNotFoundException { + PackageManager mockPackageManager = Mockito.mock(PackageManager.class); + ActivityInfo testActivityInfo = getTestActivityInfo(); + ComponentName testActivityComponentName = new ComponentName(TEST_APP_PACKAGE_NAME, + TEST_ACTIVITY_NAME); + + when(mockPackageManager.getActivityInfo(testActivityComponentName, 0)) + .thenReturn(testActivityInfo); + when(mockPackageManager.getPackageInfo(TEST_APP_PACKAGE_NAME, 0)) + .thenReturn(createPackageInfo(TEST_APP_PACKAGE_NAME, TEST_APP_VERSION_CODE, + testActivityInfo)); + when(mockPackageManager.getPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, 0)) + .thenReturn(createPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, + TEST_A11Y_SERVICE_SOURCE_VERSION_CODE, null)); + return mockPackageManager; + } + + static ActivityInfo getTestActivityInfo() { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = TEST_APP_PACKAGE_NAME; + activityInfo.name = TEST_ACTIVITY_NAME; + return activityInfo; + } + + static PackageInfo createPackageInfo(String packageName, int versionCode, + @Nullable ActivityInfo activityInfo) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = packageName; + packageInfo.setLongVersionCode(versionCode); + if (activityInfo != null) { + packageInfo.activities = new ActivityInfo[]{activityInfo}; + } + return packageInfo; + + } +} diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java index 633a3c985b7f..901c0361092f 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java @@ -54,6 +54,7 @@ import android.os.vibrator.Flags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.AtomicFile; import android.util.SparseArray; +import android.view.HapticFeedbackConstants; import androidx.test.InstrumentationRegistry; @@ -292,7 +293,7 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, /* fromIme= */ false); + effectId, /* flags */ 0, /* privFlags */ 0); assertThat(attrs.getUsage()).isEqualTo(VibrationAttributes.USAGE_COMMUNICATION_REQUEST); } } @@ -302,8 +303,7 @@ public class HapticFeedbackVibrationProviderTest { HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ false, - false /* fromIme*/); + SAFE_MODE_ENABLED, /* flags */ 0, /* privFlags */ 0); assertThat(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)).isFalse(); } @@ -313,7 +313,8 @@ public class HapticFeedbackVibrationProviderTest { HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ true, false /* fromIme*/); + SAFE_MODE_ENABLED, + /* flags */ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, /* privFlags */ 0); assertThat(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)).isTrue(); } @@ -325,7 +326,7 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : SCROLL_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/); + effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isTrue(); } @@ -338,7 +339,7 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : SCROLL_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/); + effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected no FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isFalse(); } @@ -351,7 +352,8 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/); + effectId, /* flags */ 0, + HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); assertWithMessage("Expected no CATEGORY_KEYBOARD for effect " + effectId) @@ -366,7 +368,7 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/); + effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId) @@ -381,7 +383,8 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/); + effectId, /* flags */ 0, + HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId) @@ -398,7 +401,8 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/); + effectId, /* flags */ 0, + HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse(); @@ -414,7 +418,7 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/); + effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse(); @@ -430,7 +434,8 @@ public class HapticFeedbackVibrationProviderTest { for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/); + effectId, /* flags */ 0, + HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 18752847910b..ef944dbba3ca 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -2661,9 +2661,10 @@ public class VibratorManagerServiceTest { private HalVibration performHapticFeedbackAndWaitUntilFinished(VibratorManagerService service, int constant, boolean always) throws InterruptedException { - HalVibration vib = - service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, - constant, always, "some reason", service, false /* fromIme */); + HalVibration vib = service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT, + PACKAGE_NAME, constant, "some reason", service, + always ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0 /* flags */, + 0 /* privFlags */); if (vib != null) { vib.waitForEnd(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index 564c29f09bc9..11e6d90a5f50 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -23,7 +23,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -176,22 +175,9 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); - - assertInCameraCompatMode(); - assertActivityRefreshRequested(/* refreshRequested */ true); - } - - @Test - public void testReconnectedToDifferentCamera_activatesCameraCompatModeAndRefresh() - throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertInCameraCompatMode(); @@ -199,20 +185,6 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test - public void testCameraDisconnected_deactivatesCameraCompatMode() { - configureActivityAndDisplay(SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_LANDSCAPE, - WINDOWING_MODE_FREEFORM); - // Open camera and test for compat treatment - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(); - - // Close camera and test for revert - mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); - - assertNotInCameraCompatMode(); - } - - @Test public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java index e468fd8ee495..1c8dc05c6787 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java @@ -256,8 +256,7 @@ public final class CameraStateMonitorTests extends WindowTestsBase { } @Override - public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, - @NonNull String cameraId) { + public boolean onCameraClosed(@NonNull String cameraId) { mOnCameraClosedCounter++; boolean returnValue = mOnCameraClosedReturnValue; // If false, return false only the first time, so it doesn't fall in the infinite retry diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index a4bec64bdf97..ad3fba3084fe 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -166,9 +166,9 @@ public class DesktopModeLaunchParamsModifierTests extends ACTIVITY_TYPE_STANDARD).setDisplay(display).build(); final int desiredWidth = - (int) (DISPLAY_STABLE_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); final int desiredHeight = - (int) (DISPLAY_STABLE_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); assertEquals(desiredWidth, mResult.mBounds.width()); assertEquals(desiredHeight, mResult.mBounds.height()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java index 38ad9a7e0dca..4b0668f7a056 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -270,8 +270,8 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override - public boolean performHapticFeedback(int uid, String packageName, int effectId, - boolean always, String reason, boolean fromIme) { + public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason, + int flags, int privFlags) { return false; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 95d37ebf4597..7d01b79fe866 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -35,6 +35,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_CONFIG_AT_END; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; @@ -77,13 +78,14 @@ import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import android.util.ArraySet; import android.view.SurfaceControl; @@ -95,6 +97,7 @@ import android.window.ITaskOrganizer; import android.window.ITransitionPlayer; import android.window.RemoteTransition; import android.window.SystemPerformanceHinter; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentOrganizer; import android.window.TransitionInfo; @@ -104,7 +107,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.graphics.ColorUtils; import com.android.window.flags.Flags; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -120,16 +122,15 @@ import java.util.function.Function; * Build/Install/Run: * atest WmTests:TransitionTests */ -@EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @SmallTest @Presubmit @RunWith(WindowTestRunner.class) public class TransitionTests extends WindowTestsBase { final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class); private BLASTSyncEngine mSyncEngine; + private Transition mTransition; + private TransitionInfo mInfo; - @Rule - public SetFlagsRule mRule = new SetFlagsRule(); private Transition createTestTransition(int transitType, TransitionController controller) { final Transition transition = new Transition(transitType, 0 /* flags */, controller, controller.mSyncEngine); @@ -1994,6 +1995,221 @@ public class TransitionTests extends WindowTestsBase { assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor()); } + @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_disableAnimOptionsPerChange() { + initializeOverrideAnimationOptionsTest(); + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCommonAnimOptions("testPackage"); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + assertEquals(options, mInfo.getAnimationOptions()); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_nonCustomAnimOptions() { + initializeOverrideAnimationOptionsTest(); + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCommonAnimOptions("testPackage"); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); + final TransitionInfo.Change taskChange = mInfo.getChanges().get(1); + final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2); + final TransitionInfo.Change activityChange = mInfo.getChanges().get(3); + + assertNull("Display change's AnimationOptions must not be overridden.", + displayChange.getAnimationOptions()); + assertNull("Task change's AnimationOptions must not be overridden.", + taskChange.getAnimationOptions()); + assertNull("Embedded TF change's AnimationOptions must not be overridden.", + embeddedTfChange.getAnimationOptions()); + assertEquals("Activity change's AnimationOptions must be overridden.", + options, activityChange.getAnimationOptions()); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_crossProfileAnimOptions() { + initializeOverrideAnimationOptionsTest(); + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCrossProfileAnimOptions(); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); + final TransitionInfo.Change taskChange = mInfo.getChanges().get(1); + final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2); + final TransitionInfo.Change activityChange = mInfo.getChanges().get(3); + activityChange.setMode(TRANSIT_OPEN); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + assertNull("Display change's AnimationOptions must not be overridden.", + displayChange.getAnimationOptions()); + assertNull("Task change's AnimationOptions must not be overridden.", + taskChange.getAnimationOptions()); + assertNull("Embedded TF change's AnimationOptions must not be overridden.", + embeddedTfChange.getAnimationOptions()); + assertEquals("Activity change's AnimationOptions must be overridden.", + options, activityChange.getAnimationOptions()); + assertTrue(activityChange.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL)); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptions() { + initializeOverrideAnimationOptionsTest(); + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCustomAnimOptions("testPackage", Resources.ID_NULL, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + Color.GREEN, false /* overrideTaskTransition */); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); + final TransitionInfo.Change taskChange = mInfo.getChanges().get(1); + final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2); + final TransitionInfo.Change activityChange = mInfo.getChanges().get(3); + + assertNull("Display change's AnimationOptions must not be overridden.", + displayChange.getAnimationOptions()); + assertNull("Task change's AnimationOptions must not be overridden.", + taskChange.getAnimationOptions()); + assertEquals("Embedded TF change's AnimationOptions must be overridden.", + options, embeddedTfChange.getAnimationOptions()); + assertEquals("Embedded TF change's background color must not be overridden.", + 0, embeddedTfChange.getBackgroundColor()); + assertEquals("Activity change's AnimationOptions must be overridden.", + options, activityChange.getAnimationOptions()); + assertEquals("Activity change's background color must be overridden.", + options.getBackgroundColor(), activityChange.getBackgroundColor()); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_haveTaskFragmentAnimParams() { + initializeOverrideAnimationOptionsTest(); + + final TaskFragment embeddedTf = mTransition.mTargets.get(2).mContainer.asTaskFragment(); + embeddedTf.setAnimationParams(new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(Color.RED) + .setOpenAnimationResId(0x12345678) + .build()); + + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCustomAnimOptions("testPackage", Resources.ID_NULL, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + Color.GREEN, false /* overrideTaskTransition */); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); + final TransitionInfo.Change taskChange = mInfo.getChanges().get(1); + final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2); + final TransitionInfo.Change activityChange = mInfo.getChanges().get(3); + + final int expectedColor = embeddedTf.getAnimationParams().getAnimationBackgroundColor(); + embeddedTfChange.setBackgroundColor(expectedColor); + final TransitionInfo.AnimationOptions expectedOptions = TransitionInfo.AnimationOptions + .makeCustomAnimOptions("testPackage", 0x12345678, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + 0, false /* overrideTaskTransition */); + embeddedTfChange.setAnimationOptions(expectedOptions); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + assertNull("Display change's AnimationOptions must not be overridden.", + displayChange.getAnimationOptions()); + assertNull("Task change's AnimationOptions must not be overridden.", + taskChange.getAnimationOptions()); + assertEquals("Embedded TF change's AnimationOptions must be overridden.", + expectedOptions, embeddedTfChange.getAnimationOptions()); + assertEquals("Embedded TF change's background color must not be overridden.", + expectedColor, embeddedTfChange.getBackgroundColor()); + assertEquals("Activity change's AnimationOptions must be overridden.", + options, activityChange.getAnimationOptions()); + assertEquals("Activity change's background color must be overridden.", + options.getBackgroundColor(), activityChange.getBackgroundColor()); + } + + @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) + @Test + public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptionsWithTaskOverride() { + initializeOverrideAnimationOptionsTest(); + TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions + .makeCustomAnimOptions("testPackage", Resources.ID_NULL, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, + Color.GREEN, true /* overrideTaskTransition */); + mTransition.setOverrideAnimation(options, null /* startCallback */, + null /* finishCallback */); + + mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); + + final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); + final TransitionInfo.Change taskChange = mInfo.getChanges().get(1); + final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2); + final TransitionInfo.Change activityChange = mInfo.getChanges().get(3); + + assertNull("Display change's AnimationOptions must not be overridden.", + displayChange.getAnimationOptions()); + assertEquals("Task change's AnimationOptions must be overridden.", + options, taskChange.getAnimationOptions()); + assertEquals("Task change's background color must be overridden.", + options.getBackgroundColor(), taskChange.getBackgroundColor()); + assertEquals("Embedded TF change's AnimationOptions must be overridden.", + options, embeddedTfChange.getAnimationOptions()); + assertEquals("Embedded TF change's background color must be overridden.", + 0, embeddedTfChange.getBackgroundColor()); + assertEquals("Activity change's AnimationOptions must be overridden.", + options, activityChange.getAnimationOptions()); + assertEquals("Activity change's background color must be overridden.", + options.getBackgroundColor(), activityChange.getBackgroundColor()); + } + + private void initializeOverrideAnimationOptionsTest() { + mTransition = createTestTransition(TRANSIT_OPEN); + + // Test set AnimationOptions for Activity and Task. + final Task task = createTask(mDisplayContent); + // Create an embedded TaskFragment. + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + registerTaskFragmentOrganizer( + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder())); + final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord nonEmbeddedActivity = createActivityRecord(task); + mWm.mCurrentUserId = nonEmbeddedActivity.mUserId; + + mTransition.mTargets = new ArrayList<>(); + mTransition.mTargets.add(new Transition.ChangeInfo(mDisplayContent)); + mTransition.mTargets.add(new Transition.ChangeInfo(task)); + mTransition.mTargets.add(new Transition.ChangeInfo(embeddedTf)); + mTransition.mTargets.add(new Transition.ChangeInfo(nonEmbeddedActivity)); + + mInfo = new TransitionInfo(TRANSIT_OPEN, 0 /* flags */); + mInfo.addChange(new TransitionInfo.Change(mDisplayContent.mRemoteToken + .toWindowContainerToken(), mDisplayContent.getAnimationLeash())); + mInfo.addChange(new TransitionInfo.Change(task.mRemoteToken.toWindowContainerToken(), + task.getAnimationLeash())); + mInfo.addChange(new TransitionInfo.Change(embeddedTf.mRemoteToken.toWindowContainerToken(), + embeddedTf.getAnimationLeash())); + mInfo.addChange(new TransitionInfo.Change(null /* container */, + nonEmbeddedActivity.getAnimationLeash())); + } + @Test public void testTransitionVisibleChange() { registerTestTransitionPlayer(); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index aebae4eec3f1..4b83b65e435a 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -1078,6 +1078,11 @@ public final class SatelliteManager { * @hide */ public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5; + /** + * Datagram type indicating that the message to be sent or received is of type SMS. + * @hide + */ + public static final int DATAGRAM_TYPE_SMS = 6; /** @hide */ @IntDef(prefix = "DATAGRAM_TYPE_", value = { @@ -1086,7 +1091,8 @@ public final class SatelliteManager { DATAGRAM_TYPE_LOCATION_SHARING, DATAGRAM_TYPE_KEEP_ALIVE, DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP, - DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED + DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED, + DATAGRAM_TYPE_SMS }) @Retention(RetentionPolicy.SOURCE) public @interface DatagramType {} diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 238f2afa7994..5121f6677cea 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -119,7 +119,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ): UiObject2? { if ( wmHelper.getWindow(innerHelper)?.windowingMode != - WindowingMode.WINDOWING_MODE_FREEFORM.value + WindowingMode.WINDOWING_MODE_FREEFORM.value ) error("expected a freeform window with caption but window is not in freeform mode") val captions = @@ -147,7 +147,17 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val endX = startX + horizontalChange // drag the specified corner of the window to the end coordinate. - device.drag(startX, startY, endX, endY, 100) + dragWindow(startX, startY, endX, endY, wmHelper, device) + } + + /** Drag a window from a source coordinate to a destination coordinate. */ + fun dragWindow( + startX: Int, startY: Int, + endX: Int, endY: Int, + wmHelper: WindowManagerStateHelper, + device: UiDevice + ) { + device.drag(startX, startY, endX, endY, /* steps= */ 100) wmHelper .StateSyncBuilder() .withAppTransitionIdle() diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index d0148fbbee3d..abfe549f3d22 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -136,11 +136,20 @@ class PointerIconLoadingTest { assumeTrue(enableVectorCursors()) assumeTrue(enableVectorCursorA11ySettings()) + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), + /* force= */ true) + theme.applyStyle( + PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), + /* force= */ true) val pointerScale = 2f val pointerIcon = PointerIcon.getLoadedSystemIcon( - context, + ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, pointerScale) 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 f3851790688e..5f9a710c5f78 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 @@ -54,6 +54,7 @@ import com.android.compatibility.common.util.SystemUtil; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -110,6 +111,7 @@ public final class ConcurrentMultiUserTest { } @Test + @Ignore("b/352823913") public void passengerShowImeNotAffectDriver() throws Exception { assertDriverImeHidden(); assertPassengerImeHidden(); diff --git a/tools/lint/fix/README.md b/tools/lint/fix/README.md index a5ac2be1c18a..18bda9287a50 100644 --- a/tools/lint/fix/README.md +++ b/tools/lint/fix/README.md @@ -6,7 +6,7 @@ Inspiration: go/refactor-the-platform-with-lint\ It's a python script that runs the framework linter, and then (optionally) copies modified files back into the source tree.\ -Why python, you ask? Because python is cool ¯\_(ツ)_/¯. +Why python, you ask? Because python is cool ¯\\\_(ツ)\_/¯. Incidentally, this exposes a much simpler way to run individual lint checks against individual modules, so it's useful beyond applying fixes. @@ -15,7 +15,7 @@ against individual modules, so it's useful beyond applying fixes. Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag. As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate -directory. This script runs the lint, unpacks those files, and copies them back into the tree. +directory. This script runs the lint, unpacks those files, and copies them back into the tree. ## How do I run it? **WARNING: You probably want to commit/stash any changes to your working tree before doing this...** |