diff options
246 files changed, 6531 insertions, 2128 deletions
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 4851279eea97..d0d76a4c8285 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -454,12 +454,11 @@ public class Dialog implements DialogInterface, Window.Callback, */ protected void onStart() { if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true); - if (mContext != null + if (allowsRegisterDefaultOnBackInvokedCallback() && mContext != null && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { // Add onBackPressed as default back behavior. mDefaultBackCallback = this::onBackPressed; getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback); - mDefaultBackCallback = null; } } @@ -470,9 +469,18 @@ public class Dialog implements DialogInterface, Window.Callback, if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); if (mDefaultBackCallback != null) { getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback); + mDefaultBackCallback = null; } } + /** + * Whether this dialog allows to register the default onBackInvokedCallback. + * @hide + */ + protected boolean allowsRegisterDefaultOnBackInvokedCallback() { + return true; + } + private static final String DIALOG_SHOWING_TAG = "android:dialogShowing"; private static final String DIALOG_HIERARCHY_TAG = "android:dialogHierarchy"; @@ -697,7 +705,8 @@ public class Dialog implements DialogInterface, Window.Callback, if (event.isTracking() && !event.isCanceled()) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: - if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext) + || !allowsRegisterDefaultOnBackInvokedCallback()) { onBackPressed(); return true; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 892b45ed2292..7d2ef4d9b943 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -12819,7 +12819,6 @@ public class Notification implements Parcelable } else { mBackgroundColor = rawColor; } - mProtectionColor = COLOR_INVALID; // filled in at the end mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast( ContrastColorUtil.resolvePrimaryColor(ctx, mBackgroundColor, nightMode), mBackgroundColor, 4.5); @@ -12836,7 +12835,6 @@ public class Notification implements Parcelable } else { int[] attrs = { R.attr.colorSurface, - R.attr.colorBackgroundFloating, R.attr.textColorPrimary, R.attr.textColorSecondary, R.attr.colorAccent, @@ -12848,15 +12846,14 @@ public class Notification implements Parcelable }; try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) { mBackgroundColor = getColor(ta, 0, nightMode ? Color.BLACK : Color.WHITE); - mProtectionColor = getColor(ta, 1, COLOR_INVALID); - mPrimaryTextColor = getColor(ta, 2, COLOR_INVALID); - mSecondaryTextColor = getColor(ta, 3, COLOR_INVALID); - mPrimaryAccentColor = getColor(ta, 4, COLOR_INVALID); - mSecondaryAccentColor = getColor(ta, 5, COLOR_INVALID); - mTertiaryAccentColor = getColor(ta, 6, COLOR_INVALID); - mOnAccentTextColor = getColor(ta, 7, COLOR_INVALID); - mErrorColor = getColor(ta, 8, COLOR_INVALID); - mRippleAlpha = Color.alpha(getColor(ta, 9, 0x33ffffff)); + mPrimaryTextColor = getColor(ta, 1, COLOR_INVALID); + mSecondaryTextColor = getColor(ta, 2, COLOR_INVALID); + mPrimaryAccentColor = getColor(ta, 3, COLOR_INVALID); + mSecondaryAccentColor = getColor(ta, 4, COLOR_INVALID); + mTertiaryAccentColor = getColor(ta, 5, COLOR_INVALID); + mOnAccentTextColor = getColor(ta, 6, COLOR_INVALID); + mErrorColor = getColor(ta, 7, COLOR_INVALID); + mRippleAlpha = Color.alpha(getColor(ta, 8, 0x33ffffff)); } mContrastColor = calculateContrastColor(ctx, rawColor, mPrimaryAccentColor, mBackgroundColor, nightMode); @@ -12889,9 +12886,7 @@ public class Notification implements Parcelable } } // make sure every color has a valid value - if (mProtectionColor == COLOR_INVALID) { - mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.8f); - } + mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.9f); } /** calculates the contrast color for the non-colorized notifications */ diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 2b5175ca6659..634089b73618 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -248,6 +248,13 @@ public class TaskInfo { public boolean topActivityEligibleForUserAspectRatioButton; /** + * Whether the user has forced the activity to be fullscreen through the user aspect ratio + * settings. + * @hide + */ + public boolean isUserFullscreenOverrideEnabled; + + /** * Hint about the letterbox state of the top activity. * @hide */ @@ -543,7 +550,8 @@ public class TaskInfo { && isSleeping == that.isSleeping && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId) && parentTaskId == that.parentTaskId - && Objects.equals(topActivity, that.topActivity); + && Objects.equals(topActivity, that.topActivity) + && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled; } /** @@ -574,7 +582,8 @@ public class TaskInfo { && (!hasCompatUI() || configuration.getLayoutDirection() == that.configuration.getLayoutDirection()) && (!hasCompatUI() || configuration.uiMode == that.configuration.uiMode) - && (!hasCompatUI() || isVisible == that.isVisible); + && (!hasCompatUI() || isVisible == that.isVisible) + && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled; } /** @@ -630,6 +639,7 @@ public class TaskInfo { topActivityLetterboxHorizontalPosition = source.readInt(); topActivityLetterboxWidth = source.readInt(); topActivityLetterboxHeight = source.readInt(); + isUserFullscreenOverrideEnabled = source.readBoolean(); } /** @@ -686,6 +696,7 @@ public class TaskInfo { dest.writeInt(topActivityLetterboxHorizontalPosition); dest.writeInt(topActivityLetterboxWidth); dest.writeInt(topActivityLetterboxHeight); + dest.writeBoolean(isUserFullscreenOverrideEnabled); } @Override @@ -732,6 +743,7 @@ public class TaskInfo { + topActivityLetterboxHorizontalPosition + " topActivityLetterboxWidth=" + topActivityLetterboxWidth + " topActivityLetterboxHeight=" + topActivityLetterboxHeight + + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled + " locusId=" + mTopActivityLocusId + " displayAreaFeatureId=" + displayAreaFeatureId + " cameraCompatControlState=" diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index c3df17d4b53e..529363f828bb 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -344,15 +344,22 @@ public class CrossProfileApps { // If there is a label for the launcher intent, then use that as it is typically shorter. // Otherwise, just use the top-level application name. Intent launchIntent = pm.getLaunchIntentForPackage(mContext.getPackageName()); + if (launchIntent == null) { + return getDefaultCallingApplicationLabel(); + } List<ResolveInfo> infos = pm.queryIntentActivities( launchIntent, PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY)); if (infos.size() > 0) { return infos.get(0).loadLabel(pm); } + return getDefaultCallingApplicationLabel(); + } + + private CharSequence getDefaultCallingApplicationLabel() { return mContext.getApplicationInfo() .loadSafeLabel( - pm, + mContext.getPackageManager(), /* ellipsizeDip= */ 0, TextUtils.SAFE_STRING_FLAG_SINGLE_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index c5585afb143a..ea0f5ff2896e 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -304,6 +304,8 @@ interface IPackageManager { boolean isPackageSuspendedForUser(String packageName, int userId); + boolean isPackageQuarantinedForUser(String packageName, int userId); + Bundle getSuspendedPackageAppExtras(String packageName, int userId); /** diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 795eb4a737ef..8f653b3808c1 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -576,6 +576,12 @@ public class InputMethodService extends AbstractInputMethodService { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final long DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE = 148086656L; + /** + * Enable the logic to allow hiding the IME caption bar ("fake" IME navigation bar). + * @hide + */ + public static final boolean ENABLE_HIDE_IME_CAPTION_BAR = true; + LayoutInflater mInflater; TypedArray mThemeAttrs; @UnsupportedAppUsage diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 78388efe98c7..c01664e55744 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -16,6 +16,8 @@ package android.inputmethodservice; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; +import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import android.animation.ValueAnimator; @@ -230,6 +232,16 @@ final class NavigationBarController { setIconTintInternal(calculateTargetDarkIntensity(mAppearance, mDrawLegacyNavigationBarBackground)); + + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> { + if (mNavigationBarFrame != null) { + boolean visible = insets.isVisible(captionBar()); + mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + return view.onApplyWindowInsets(insets); + }); + } } private void uninstallNavigationBarFrameIfNecessary() { @@ -240,6 +252,9 @@ final class NavigationBarController { if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mNavigationBarFrame); } + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setOnApplyWindowInsetsListener(null); + } mNavigationBarFrame = null; } @@ -414,7 +429,9 @@ final class NavigationBarController { decor.bringChildToFront(mNavigationBarFrame); } } - mNavigationBarFrame.setVisibility(View.VISIBLE); + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setVisibility(View.VISIBLE); + } } } @@ -435,6 +452,11 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mService.mWindow.getWindow().getDecorView().getWindowInsetsController() + .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight()); + } + if (imeDrawsImeNavBar) { installNavigationBarFrameIfNecessary(); if (mNavigationBarFrame == null) { @@ -528,6 +550,16 @@ final class NavigationBarController { return drawLegacyNavigationBarBackground; } + /** + * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead. + */ + private int getImeCaptionBarHeight() { + return mImeDrawsImeNavBar + ? mService.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_frame_height) + : 0; + } + @Override public String toDebugString() { return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index 5704dac7a327..e4a09a651ae1 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -79,6 +79,13 @@ final class SoftInputWindow extends Dialog { @WindowState private int mWindowState = WindowState.TOKEN_PENDING; + @Override + protected boolean allowsRegisterDefaultOnBackInvokedCallback() { + // Do not register OnBackInvokedCallback from Dialog#onStart, InputMethodService will + // register CompatOnBackInvokedCallback for input method window. + return false; + } + /** * Set {@link IBinder} window token to the window. * diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index b24b45d11c3a..08b32bf2b9e0 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -525,14 +525,14 @@ public abstract class VibrationEffect implements Parcelable { public abstract long getDuration(); /** - * Checks if a given {@link Vibrator} can play this effect as intended. + * Checks if a vibrator with a given {@link VibratorInfo} can play this effect as intended. * - * <p>See @link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more information - * about what counts as supported by a vibrator, and what counts as not. + * <p>See {@link VibratorInfo#areVibrationFeaturesSupported(VibrationEffect)} for more + * information about what counts as supported by a vibrator, and what counts as not. * * @hide */ - public abstract boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator); + public abstract boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo); /** * Returns true if this effect could represent a touch haptic feedback. @@ -813,9 +813,9 @@ public abstract class VibrationEffect implements Parcelable { /** @hide */ @Override - public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) { + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { for (VibrationEffectSegment segment : mSegments) { - if (!segment.areVibrationFeaturesSupported(vibrator)) { + if (!segment.areVibrationFeaturesSupported(vibratorInfo)) { return false; } } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 4e852e333ec8..79e0ca87eade 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -216,9 +216,7 @@ public abstract class Vibrator { */ @TestApi public boolean hasFrequencyControl() { - // We currently can only control frequency of the vibration using the compose PWLE method. - return getInfo().hasCapability( - IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + return getInfo().hasFrequencyControl(); } /** @@ -240,7 +238,7 @@ public abstract class Vibrator { * @hide */ public boolean areVibrationFeaturesSupported(@NonNull VibrationEffect effect) { - return effect.areVibrationFeaturesSupported(this); + return getInfo().areVibrationFeaturesSupported(effect); } /** diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java index 02e685699398..0b7d7c3cb877 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -242,6 +242,17 @@ public class VibratorInfo implements Parcelable { } /** + * Check whether the vibrator has frequency control. + * + * @return True if the hardware can control the frequency of the vibrations, otherwise false. + */ + public boolean hasFrequencyControl() { + // We currently can only control frequency of the vibration using the compose PWLE method. + return hasCapability( + IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + } + + /** * Returns a default value to be applied to composed PWLE effects for braking. * * @return a supported braking value, one of android.hardware.vibrator.Braking.* @@ -323,6 +334,23 @@ public class VibratorInfo implements Parcelable { } /** + * Query whether or not the vibrator supports all components of a given {@link VibrationEffect} + * (i.e. the vibrator can play the given effect as intended). + * + * <p>See {@link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more + * information on how the vibrator support is determined. + * + * @param effect the {@link VibrationEffect} to check if it is supported + * @return {@code true} if the vibrator can play the given {@code effect} as intended, + * {@code false} otherwise. + * + * @hide + */ + public boolean areVibrationFeaturesSupported(@NonNull VibrationEffect effect) { + return effect.areVibrationFeaturesSupported(this); + } + + /** * Query the estimated duration of given primitive. * * @param primitiveId Which primitives to query for. diff --git a/core/java/android/os/storage/StorageManagerInternal.java b/core/java/android/os/storage/StorageManagerInternal.java index 059bd846327c..22e8251b3f52 100644 --- a/core/java/android/os/storage/StorageManagerInternal.java +++ b/core/java/android/os/storage/StorageManagerInternal.java @@ -19,6 +19,7 @@ package android.os.storage; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.pm.UserInfo; import android.os.IVold; import java.util.List; @@ -169,4 +170,19 @@ public abstract class StorageManagerInternal { */ public abstract void registerCloudProviderChangeListener( @NonNull CloudProviderChangeListener listener); + + /** + * Prepares user data directories before moving storage or apps. This is required as adoptable + * storage unlock is tied to the prepare user data and storage needs to be unlocked before + * performing any operations on it. This will also create user data directories before + * initiating the move operations, which essential for ensuring the directories to have correct + * SELinux labels and permissions. + * + * @param fromVolumeUuid the source volume UUID from which content needs to be transferred + * @param toVolumeUuid the destination volume UUID to which contents are to be transferred + * @param users a list of users for whom to prepare storage + */ + public abstract void prepareUserStorageForMove(String fromVolumeUuid, String toVolumeUuid, + List<UserInfo> users); + } diff --git a/core/java/android/os/vibrator/PrebakedSegment.java b/core/java/android/os/vibrator/PrebakedSegment.java index 42b6c2dae5eb..a035092e314f 100644 --- a/core/java/android/os/vibrator/PrebakedSegment.java +++ b/core/java/android/os/vibrator/PrebakedSegment.java @@ -23,6 +23,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorInfo; import java.util.Objects; @@ -77,8 +78,8 @@ public final class PrebakedSegment extends VibrationEffectSegment { /** @hide */ @Override - public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) { - if (vibrator.areAllEffectsSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) { + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { + if (vibratorInfo.isEffectSupported(mEffectId) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) { return true; } if (!mFallback) { diff --git a/core/java/android/os/vibrator/PrimitiveSegment.java b/core/java/android/os/vibrator/PrimitiveSegment.java index c52a09ce69c7..95d97bfe4ad1 100644 --- a/core/java/android/os/vibrator/PrimitiveSegment.java +++ b/core/java/android/os/vibrator/PrimitiveSegment.java @@ -22,7 +22,7 @@ import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; import com.android.internal.util.Preconditions; @@ -77,8 +77,8 @@ public final class PrimitiveSegment extends VibrationEffectSegment { /** @hide */ @Override - public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) { - return vibrator.areAllPrimitivesSupported(mPrimitiveId); + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { + return vibratorInfo.isPrimitiveSupported(mPrimitiveId); } /** @hide */ diff --git a/core/java/android/os/vibrator/RampSegment.java b/core/java/android/os/vibrator/RampSegment.java index e997bcdbdb32..5f9d1024d9a5 100644 --- a/core/java/android/os/vibrator/RampSegment.java +++ b/core/java/android/os/vibrator/RampSegment.java @@ -20,7 +20,7 @@ import android.annotation.NonNull; import android.annotation.TestApi; import android.os.Parcel; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; import com.android.internal.util.Preconditions; @@ -96,7 +96,7 @@ public final class RampSegment extends VibrationEffectSegment { /** @hide */ @Override - public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) { + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { boolean areFeaturesSupported = true; // If the start/end frequencies are not the same, require frequency control since we need to // ramp up/down the frequency. @@ -104,7 +104,7 @@ public final class RampSegment extends VibrationEffectSegment { // If there is no frequency ramping, make sure that the one frequency used does not // require frequency control. || frequencyRequiresFrequencyControl(mStartFrequencyHz)) { - areFeaturesSupported &= vibrator.hasFrequencyControl(); + areFeaturesSupported &= vibratorInfo.hasFrequencyControl(); } // If the start/end amplitudes are not the same, require amplitude control since we need to // ramp up/down the amplitude. @@ -112,7 +112,7 @@ public final class RampSegment extends VibrationEffectSegment { // If there is no amplitude ramping, make sure that the amplitude used does not // require amplitude control. || amplitudeRequiresAmplitudeControl(mStartAmplitude)) { - areFeaturesSupported &= vibrator.hasAmplitudeControl(); + areFeaturesSupported &= vibratorInfo.hasAmplitudeControl(); } return areFeaturesSupported; } diff --git a/core/java/android/os/vibrator/StepSegment.java b/core/java/android/os/vibrator/StepSegment.java index a585aa866ed8..9576a5bba1f1 100644 --- a/core/java/android/os/vibrator/StepSegment.java +++ b/core/java/android/os/vibrator/StepSegment.java @@ -21,7 +21,7 @@ import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; import com.android.internal.util.Preconditions; @@ -82,13 +82,13 @@ public final class StepSegment extends VibrationEffectSegment { /** @hide */ @Override - public boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator) { + public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) { boolean areFeaturesSupported = true; if (frequencyRequiresFrequencyControl(mFrequencyHz)) { - areFeaturesSupported &= vibrator.hasFrequencyControl(); + areFeaturesSupported &= vibratorInfo.hasFrequencyControl(); } if (amplitudeRequiresAmplitudeControl(mAmplitude)) { - areFeaturesSupported &= vibrator.hasAmplitudeControl(); + areFeaturesSupported &= vibratorInfo.hasAmplitudeControl(); } return areFeaturesSupported; } diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java index 3b286a7228fb..17ac36f3ab37 100644 --- a/core/java/android/os/vibrator/VibrationEffectSegment.java +++ b/core/java/android/os/vibrator/VibrationEffectSegment.java @@ -21,7 +21,7 @@ import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; /** * Representation of a single segment of a {@link VibrationEffect}. @@ -65,7 +65,7 @@ public abstract class VibrationEffectSegment implements Parcelable { * * @hide */ - public abstract boolean areVibrationFeaturesSupported(@NonNull Vibrator vibrator); + public abstract boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo); /** * Returns true if this segment could be a haptic feedback effect candidate. diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS index f6b123554367..4e8d6e70091c 100644 --- a/core/java/android/security/OWNERS +++ b/core/java/android/security/OWNERS @@ -6,5 +6,4 @@ per-file NetworkSecurityPolicy.java = cbrubaker@google.com per-file NetworkSecurityPolicy.java = klyubin@google.com per-file FrameworkNetworkSecurityPolicy.java = cbrubaker@google.com per-file FrameworkNetworkSecurityPolicy.java = klyubin@google.com -per-file Confirmation*.java = jdanis@google.com -per-file Confirmation*.java = swillden@google.com +per-file Confirmation*.java = file:/keystore/OWNERS diff --git a/core/java/android/security/keystore/OWNERS b/core/java/android/security/keystore/OWNERS index 65129a46d113..d9e01161ce6d 100644 --- a/core/java/android/security/keystore/OWNERS +++ b/core/java/android/security/keystore/OWNERS @@ -1,5 +1 @@ -# Bug component: 189335 - -swillden@google.com -jdanis@google.com -jbires@google.com +include /keystore/OWNERS diff --git a/core/java/android/security/keystore/recovery/OWNERS b/core/java/android/security/keystore/recovery/OWNERS deleted file mode 100644 index 65129a46d113..000000000000 --- a/core/java/android/security/keystore/recovery/OWNERS +++ /dev/null @@ -1,5 +0,0 @@ -# Bug component: 189335 - -swillden@google.com -jdanis@google.com -jbires@google.com diff --git a/core/java/android/speech/OWNERS b/core/java/android/speech/OWNERS index 162e02904075..0f2f8ad3d99e 100644 --- a/core/java/android/speech/OWNERS +++ b/core/java/android/speech/OWNERS @@ -1,5 +1,4 @@ volnov@google.com eugeniom@google.com schfan@google.com -andreaambu@google.com -hackz@google.com
\ No newline at end of file +hackz@google.com diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 3bdaca9a9ba5..e287bd9165ce 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -622,7 +622,7 @@ public class DynamicLayout extends Layout { sBuilder = null; } - if (reflowed == null) { + if (b == null) { b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); } @@ -641,7 +641,7 @@ public class DynamicLayout extends Layout { .setAddLastLineLineSpacing(!islast) .setIncludePad(false); - reflowed = b.regenerate(true /* trackpadding */, reflowed); + reflowed = b.buildPartialStaticLayoutForDynamicLayout(true /* trackpadding */, reflowed); int n = reflowed.getLineCount(); // If the new layout has a blank line at the end, but it is not // the very end of the buffer, then we already have a line that diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f843900e8022..3d1895c3eaf3 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -437,13 +437,25 @@ public class StaticLayout extends Layout { return result; } - /* package */ @NonNull StaticLayout regenerate(boolean trackpadding, StaticLayout recycle) { + /** + * DO NOT USE THIS METHOD OTHER THAN DynamicLayout. + * + * This class generates a very weird StaticLayout only for getting a result of line break. + * Since DynamicLayout keeps StaticLayout reference in the static context for object + * recycling but keeping text reference in static context will end up with leaking Context + * due to TextWatcher via TextView. + * + * So, this is a dirty work around that creating StaticLayout without passing text reference + * to the super constructor, but calculating the text layout by calling generate function + * directly. + */ + /* package */ @NonNull StaticLayout buildPartialStaticLayoutForDynamicLayout( + boolean trackpadding, StaticLayout recycle) { if (recycle == null) { - return new StaticLayout(this, trackpadding, COLUMNS_ELLIPSIZE); - } else { - recycle.generate(this, mIncludePad, trackpadding); - return recycle; + recycle = new StaticLayout(); } + recycle.generate(this, mIncludePad, trackpadding); + return recycle; } private CharSequence mText; @@ -474,6 +486,37 @@ public class StaticLayout extends Layout { } /** + * DO NOT USE THIS CONSTRUCTOR OTHER THAN FOR DYNAMIC LAYOUT. + * See Builder#buildPartialStaticLayoutForDynamicLayout for the reason of this constructor. + */ + private StaticLayout() { + super( + null, // text + null, // paint + 0, // width + null, // alignment + null, // textDir + 1, // spacing multiplier + 0, // spacing amount + false, // include font padding + false, // fallback line spacing + 0, // ellipsized width + null, // ellipsize + 1, // maxLines + BREAK_STRATEGY_SIMPLE, + HYPHENATION_FREQUENCY_NONE, + null, // leftIndents + null, // rightIndents + JUSTIFICATION_MODE_NONE, + null // lineBreakConfig + ); + + mColumns = COLUMNS_ELLIPSIZE; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); + mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); + } + + /** * @deprecated Use {@link Builder} instead. */ @Deprecated diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 4ecfc4044b1d..c6d8bd18bc28 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -16,10 +16,12 @@ package android.view; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; import static android.os.Trace.TRACE_TAG_VIEW; import static android.view.InsetsControllerProto.CONTROL; import static android.view.InsetsControllerProto.STATE; import static android.view.InsetsSource.ID_IME; +import static android.view.InsetsSource.ID_IME_CAPTION_BAR; import static android.view.ViewRootImpl.CAPTION_ON_SHELL; import static android.view.WindowInsets.Type.FIRST; import static android.view.WindowInsets.Type.LAST; @@ -40,6 +42,7 @@ import android.app.ActivityThread; import android.content.Context; import android.content.res.CompatibilityInfo; import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.os.CancellationSignal; import android.os.Handler; @@ -652,6 +655,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private int mLastWindowingMode; private boolean mStartingAnimation; private int mCaptionInsetsHeight = 0; + private int mImeCaptionBarInsetsHeight = 0; private boolean mAnimationsDisabled; private boolean mCompatSysUiVisibilityStaled; @@ -693,6 +697,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (!CAPTION_ON_SHELL && source1.getType() == captionBar()) { return; } + if (source1.getId() == ID_IME_CAPTION_BAR) { + return; + } // Don't change the indexes of the sources while traversing. Remove it later. mPendingRemoveIndexes.add(index1); @@ -823,6 +830,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (mFrame.equals(frame)) { return; } + if (mImeCaptionBarInsetsHeight != 0) { + setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight); + } mHost.notifyInsetsChanged(); mFrame.set(frame); } @@ -1007,6 +1017,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // Ensure to update all existing source consumers for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); + if (consumer.getId() == ID_IME_CAPTION_BAR) { + // The inset control for the IME caption bar will never be dispatched + // by the server. + continue; + } + final InsetsSourceControl control = mTmpControlArray.get(consumer.getId()); if (control != null) { controllableTypes |= control.getType(); @@ -1499,7 +1515,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation continue; } final InsetsSourceControl control = consumer.getControl(); - if (control != null && control.getLeash() != null) { + if (control != null + && (control.getLeash() != null || control.getId() == ID_IME_CAPTION_BAR)) { controls.put(control.getId(), new InsetsSourceControl(control)); typesReady |= consumer.getType(); } @@ -1885,6 +1902,35 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } @Override + public void setImeCaptionBarInsetsHeight(int height) { + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + return; + } + Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom); + InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR); + if (mImeCaptionBarInsetsHeight != height + || (source != null && !newFrame.equals(source.getFrame()))) { + mImeCaptionBarInsetsHeight = height; + if (mImeCaptionBarInsetsHeight != 0) { + mState.getOrCreateSource(ID_IME_CAPTION_BAR, captionBar()) + .setFrame(newFrame); + getSourceConsumer(ID_IME_CAPTION_BAR, captionBar()).setControl( + new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(), + null /* leash */, false /* initialVisible */, + new Point(), Insets.NONE), + new int[1], new int[1]); + } else { + mState.removeSource(ID_IME_CAPTION_BAR); + InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR); + if (sourceConsumer != null) { + sourceConsumer.setControl(null, new int[1], new int[1]); + } + } + mHost.notifyInsetsChanged(); + } + } + + @Override public void setSystemBarsBehavior(@Behavior int behavior) { mHost.setSystemBarsBehavior(behavior); } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 64411866f020..ff009ed09329 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -20,6 +20,7 @@ import static android.view.InsetsSourceProto.FRAME; import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; +import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.ime; import android.annotation.IntDef; @@ -47,6 +48,9 @@ public class InsetsSource implements Parcelable { /** The insets source ID of IME */ public static final int ID_IME = createId(null, 0, ime()); + /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */ + static final int ID_IME_CAPTION_BAR = + InsetsSource.createId(null /* owner */, 1 /* index */, captionBar()); /** * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't @@ -215,8 +219,12 @@ public class InsetsSource implements Parcelable { // During drag-move and drag-resizing, the caption insets position may not get updated // before the app frame get updated. To layout the app content correctly during drag events, // we always return the insets with the corresponding height covering the top. + // However, with the "fake" IME navigation bar treated as a caption bar, we return the + // insets with the corresponding height the bottom. if (getType() == WindowInsets.Type.captionBar()) { - return Insets.of(0, frame.height(), 0, 0); + return getId() == ID_IME_CAPTION_BAR + ? Insets.of(0, 0, 0, frame.height()) + : Insets.of(0, frame.height(), 0, 0); } // Checks for whether there is shared edge with insets for 0-width/height window. final boolean hasIntersection = relativeFrame.isEmpty() diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java index e8f62fc0963f..a4cbc52416b3 100644 --- a/core/java/android/view/PendingInsetsController.java +++ b/core/java/android/view/PendingInsetsController.java @@ -44,6 +44,7 @@ public class PendingInsetsController implements WindowInsetsController { private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners = new ArrayList<>(); private int mCaptionInsetsHeight = 0; + private int mImeCaptionBarInsetsHeight = 0; private WindowInsetsAnimationControlListener mLoggingListener; private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); @@ -91,6 +92,11 @@ public class PendingInsetsController implements WindowInsetsController { } @Override + public void setImeCaptionBarInsetsHeight(int height) { + mImeCaptionBarInsetsHeight = height; + } + + @Override public void setSystemBarsBehavior(int behavior) { if (mReplayedInsetsController != null) { mReplayedInsetsController.setSystemBarsBehavior(behavior); @@ -168,6 +174,9 @@ public class PendingInsetsController implements WindowInsetsController { if (mCaptionInsetsHeight != 0) { controller.setCaptionInsetsHeight(mCaptionInsetsHeight); } + if (mImeCaptionBarInsetsHeight != 0) { + controller.setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight); + } if (mAnimationsDisabled) { controller.setAnimationsDisabled(true); } diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index bc0bab7b5e95..cc2cd7982841 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -250,6 +250,16 @@ public interface WindowInsetsController { void setCaptionInsetsHeight(int height); /** + * Sets the insets height for the IME caption bar, which corresponds to the + * "fake" IME navigation bar. + * + * @param height the insets height of the IME caption bar. + * @hide + */ + default void setImeCaptionBarInsetsHeight(int height) { + } + + /** * Controls the behavior of system bars. * * @param behavior Determines how the bars behave when being hidden by the application. diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 7dda91d7b25e..5b6b36043684 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -410,9 +410,11 @@ public class ConversationLayout extends FrameLayout // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding // if they exist final List<MessagingMessage> newMessagingMessages = - createMessages(newMessages, false /* isHistoric */); + createMessages(newMessages, /* isHistoric= */false, + /* usePrecomputedText= */false); final List<MessagingMessage> newHistoricMessagingMessages = - createMessages(newHistoricMessages, true /* isHistoric */); + createMessages(newHistoricMessages, /* isHistoric= */true, + /* usePrecomputedText= */false); // bind it, baby bindViews(user, showSpinner, unreadCount, newMessagingMessages, @@ -981,15 +983,17 @@ public class ConversationLayout extends FrameLayout * @param newMessages the messages to parse. */ private List<MessagingMessage> createMessages( - List<Notification.MessagingStyle.Message> newMessages, boolean historic) { + List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, + boolean usePrecomputedText) { List<MessagingMessage> result = new ArrayList<>(); for (int i = 0; i < newMessages.size(); i++) { Notification.MessagingStyle.Message m = newMessages.get(i); MessagingMessage message = findAndRemoveMatchingMessage(m); if (message == null) { - message = MessagingMessage.createMessage(this, m, mImageResolver); + message = MessagingMessage.createMessage(this, m, + mImageResolver, usePrecomputedText); } - message.setIsHistoric(historic); + message.setIsHistoric(isHistoric); result.add(message); } return result; diff --git a/core/java/com/android/internal/widget/MessagingImageMessage.java b/core/java/com/android/internal/widget/MessagingImageMessage.java index 098bce14e619..c132d6a90f6c 100644 --- a/core/java/com/android/internal/widget/MessagingImageMessage.java +++ b/core/java/com/android/internal/widget/MessagingImageMessage.java @@ -93,8 +93,9 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage } @Override - public boolean setMessage(Notification.MessagingStyle.Message message) { - MessagingMessage.super.setMessage(message); + public boolean setMessage(Notification.MessagingStyle.Message message, + boolean usePrecomputedText) { + MessagingMessage.super.setMessage(message, usePrecomputedText); Drawable drawable; try { Uri uri = message.getDataUri(); @@ -114,32 +115,42 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage } mDrawable = drawable; mAspectRatio = ((float) mDrawable.getIntrinsicWidth()) / intrinsicHeight; - setImageDrawable(drawable); - setContentDescription(message.getText()); + if (!usePrecomputedText) { + finalizeInflate(); + } return true; } static MessagingMessage createMessage(IMessagingLayout layout, - Notification.MessagingStyle.Message m, ImageResolver resolver) { + Notification.MessagingStyle.Message m, ImageResolver resolver, + boolean usePrecomputedText) { MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout(); MessagingImageMessage createdMessage = sInstancePool.acquire(); if (createdMessage == null) { createdMessage = (MessagingImageMessage) LayoutInflater.from( layout.getContext()).inflate( - R.layout.notification_template_messaging_image_message, - messagingLinearLayout, - false); + R.layout.notification_template_messaging_image_message, + messagingLinearLayout, + false); createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); } createdMessage.setImageResolver(resolver); - boolean created = createdMessage.setMessage(m); - if (!created) { + // MessagingImageMessage does not use usePrecomputedText. + boolean populated = createdMessage.setMessage(m, /* usePrecomputedText= */false); + if (!populated) { createdMessage.recycle(); - return MessagingTextMessage.createMessage(layout, m); + return MessagingTextMessage.createMessage(layout, m, usePrecomputedText); } return createdMessage; } + + @Override + public void finalizeInflate() { + setImageDrawable(mDrawable); + setContentDescription(getMessage().getText()); + } + private void setImageResolver(ImageResolver resolver) { mImageResolver = resolver; } diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 8345c5cc9ef9..83557cd8a719 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -178,9 +178,9 @@ public class MessagingLayout extends FrameLayout extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages, - true /* isHistoric */); + /* isHistoric= */true, /* usePrecomputedText= */ false); final List<MessagingMessage> newMessagingMessages = - createMessages(newMessages, false /* isHistoric */); + createMessages(newMessages, /* isHistoric= */false, /* usePrecomputedText= */false); bindViews(user, showSpinner, historicMessagingMessages, newMessagingMessages); } @@ -518,15 +518,17 @@ public class MessagingLayout extends FrameLayout * @param newMessages the messages to parse. */ private List<MessagingMessage> createMessages( - List<Notification.MessagingStyle.Message> newMessages, boolean historic) { + List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, + boolean usePrecomputedText) { List<MessagingMessage> result = new ArrayList<>(); for (int i = 0; i < newMessages.size(); i++) { Notification.MessagingStyle.Message m = newMessages.get(i); MessagingMessage message = findAndRemoveMatchingMessage(m); if (message == null) { - message = MessagingMessage.createMessage(this, m, mImageResolver); + message = MessagingMessage.createMessage(this, m, + mImageResolver, usePrecomputedText); } - message.setIsHistoric(historic); + message.setIsHistoric(isHistoric); result.add(message); } return result; diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java index 5ecd3b82053d..ad90a63ab187 100644 --- a/core/java/com/android/internal/widget/MessagingMessage.java +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -34,11 +34,12 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { String IMAGE_MIME_TYPE_PREFIX = "image/"; static MessagingMessage createMessage(IMessagingLayout layout, - Notification.MessagingStyle.Message m, ImageResolver resolver) { + Notification.MessagingStyle.Message m, ImageResolver resolver, + boolean usePrecomputedText) { if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) { - return MessagingImageMessage.createMessage(layout, m, resolver); + return MessagingImageMessage.createMessage(layout, m, resolver, usePrecomputedText); } else { - return MessagingTextMessage.createMessage(layout, m); + return MessagingTextMessage.createMessage(layout, m, usePrecomputedText); } } @@ -55,9 +56,11 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { /** * Set a message for this view. + * * @return true if setting the message worked */ - default boolean setMessage(Notification.MessagingStyle.Message message) { + default boolean setMessage(Notification.MessagingStyle.Message message, + boolean usePrecomputedText) { getState().setMessage(message); return true; } @@ -151,4 +154,10 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { void setVisibility(int visibility); int getVisibility(); + + /** + * Finalize inflation of the MessagingMessages, which should be called on Main Thread. + * @hide + */ + void finalizeInflate(); } diff --git a/core/java/com/android/internal/widget/MessagingTextMessage.java b/core/java/com/android/internal/widget/MessagingTextMessage.java index 19791dbad31e..bd62aad15b34 100644 --- a/core/java/com/android/internal/widget/MessagingTextMessage.java +++ b/core/java/com/android/internal/widget/MessagingTextMessage.java @@ -23,7 +23,9 @@ import android.annotation.StyleRes; import android.app.Notification; import android.content.Context; import android.text.Layout; +import android.text.PrecomputedText; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.widget.RemoteViews; @@ -35,10 +37,13 @@ import com.android.internal.R; @RemoteViews.RemoteView public class MessagingTextMessage extends ImageFloatingTextView implements MessagingMessage { + private static final String TAG = "MessagingTextMessage"; private static final MessagingPool<MessagingTextMessage> sInstancePool = new MessagingPool<>(20); private final MessagingMessageState mState = new MessagingMessageState(this); + private PrecomputedText mPrecomputedText = null; + public MessagingTextMessage(@NonNull Context context) { super(context); } @@ -63,25 +68,32 @@ public class MessagingTextMessage extends ImageFloatingTextView implements Messa } @Override - public boolean setMessage(Notification.MessagingStyle.Message message) { - MessagingMessage.super.setMessage(message); - setText(message.getText()); + public boolean setMessage(Notification.MessagingStyle.Message message, + boolean usePrecomputedText) { + MessagingMessage.super.setMessage(message, usePrecomputedText); + if (usePrecomputedText) { + mPrecomputedText = PrecomputedText.create(message.getText(), getTextMetricsParams()); + } else { + setText(message.getText()); + mPrecomputedText = null; + } + return true; } static MessagingMessage createMessage(IMessagingLayout layout, - Notification.MessagingStyle.Message m) { + Notification.MessagingStyle.Message m, boolean usePrecomputedText) { MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout(); MessagingTextMessage createdMessage = sInstancePool.acquire(); if (createdMessage == null) { createdMessage = (MessagingTextMessage) LayoutInflater.from( layout.getContext()).inflate( - R.layout.notification_template_messaging_text_message, - messagingLinearLayout, - false); + R.layout.notification_template_messaging_text_message, + messagingLinearLayout, + false); createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); } - createdMessage.setMessage(m); + createdMessage.setMessage(m, usePrecomputedText); return createdMessage; } @@ -135,4 +147,20 @@ public class MessagingTextMessage extends ImageFloatingTextView implements Messa public void setColor(int color) { setTextColor(color); } + + @Override + public void finalizeInflate() { + try { + setText(mPrecomputedText != null ? mPrecomputedText + : getState().getMessage().getText()); + } catch (IllegalArgumentException exception) { + Log.wtf( + /* tag = */ TAG, + /* msg = */ "PrecomputedText setText failed for TextView:" + this, + /* tr = */ exception + ); + mPrecomputedText = null; + setText(getState().getMessage().getText()); + } + } } diff --git a/core/res/res/drawable/focus_event_rotary_input_background.xml b/core/res/res/drawable/focus_event_rotary_input_background.xml new file mode 100644 index 000000000000..512cd687f2b1 --- /dev/null +++ b/core/res/res/drawable/focus_event_rotary_input_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:name="focus_event_rotary_input_background" + android:shape="rectangle"> + + <!-- View background color --> + <solid android:color="#80741b47" /> + + <!-- View border color and width --> + <stroke android:width="1dp" android:color="#ffff00ff" /> + + <!-- The radius makes the corners rounded --> + <corners android:radius="4dp" /> + +</shape>
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9621f93b9d05..a78b0407be67 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -45,9 +45,7 @@ <item><xliff:g id="id">@string/status_bar_phone_signal</xliff:g></item> <item><xliff:g id="id">@string/status_bar_secure</xliff:g></item> <item><xliff:g id="id">@string/status_bar_managed_profile</xliff:g></item> - <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item> <item><xliff:g id="id">@string/status_bar_connected_display</xliff:g></item> - <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item> <item><xliff:g id="id">@string/status_bar_vpn</xliff:g></item> <item><xliff:g id="id">@string/status_bar_bluetooth</xliff:g></item> <item><xliff:g id="id">@string/status_bar_camera</xliff:g></item> @@ -56,6 +54,8 @@ <item><xliff:g id="id">@string/status_bar_mute</xliff:g></item> <item><xliff:g id="id">@string/status_bar_volume</xliff:g></item> <item><xliff:g id="id">@string/status_bar_zen</xliff:g></item> + <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item> + <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item> <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> @@ -5041,6 +5041,21 @@ </array> <!-- See DisplayWhiteBalanceController. + A float array containing a list of ambient brightnesses, in Lux. This array, + together with config_displayWhiteBalanceLowLightAmbientBiasesStrong, is used to generate a + lookup table used in DisplayWhiteBalanceController. This lookup table is used to map + ambient brightness readings to a bias, where the bias is used to linearly interpolate + between ambient color temperature and + config_displayWhiteBalanceLowLightAmbientColorTemperatureIdle. + This table is optional. If used, this array must, + 1) Contain at least two entries + 2) Be the same length as config_displayWhiteBalanceLowLightAmbientBiasesStrong. --> + <array name ="config_displayWhiteBalanceLowLightAmbientBrightnessesStrong"> + <item>10.0</item> + <item>10.0</item> + </array> + + <!-- See DisplayWhiteBalanceController. An array containing a list of biases. See config_displayWhiteBalanceLowLightAmbientBrightnesses for additional details. This array must be in the range of [0.0, 1.0]. --> @@ -5050,12 +5065,28 @@ </array> <!-- See DisplayWhiteBalanceController. + An array containing a list of biases. See + config_displayWhiteBalanceLowLightAmbientBrightnessesStrong for additional details. + This array must be in the range of [0.0, 1.0]. --> + <array name ="config_displayWhiteBalanceLowLightAmbientBiasesStrong"> + <item>0.0</item> + <item>1.0</item> + </array> + + <!-- See DisplayWhiteBalanceController. The ambient color temperature (in cct) to which we interpolate towards using the the look up table generated by config_displayWhiteBalanceLowLightAmbientBrightnesses and config_displayWhiteBalanceLowLightAmbientBiases. --> <item name="config_displayWhiteBalanceLowLightAmbientColorTemperature" format="float" type="dimen">6500.0</item> <!-- See DisplayWhiteBalanceController. + The ambient color temperature (in cct) to which we interpolate towards using the + the look up table generated by config_displayWhiteBalanceLowLightAmbientBrightnessesStrong + and config_displayWhiteBalanceLowLightAmbientBiasesStrong. Used when device is in Idle Screen + Brightness mode. --> + <item name="config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong" format="float" type="dimen">6500.0</item> + + <!-- See DisplayWhiteBalanceController. A float array containing a list of ambient brightnesses, in Lux. This array, together with config_displayWhiteBalanceHighLightAmbientBiases, is used to generate a lookup table used in DisplayWhiteBalanceController. This lookup table is used to map @@ -5069,6 +5100,19 @@ </array> <!-- See DisplayWhiteBalanceController. + A float array containing a list of ambient brightnesses, in Lux. This array, + together with config_displayWhiteBalanceHighLightAmbientBiasesStrong, is used to generate a + lookup table used in DisplayWhiteBalanceController. This lookup table is used to map + ambient brightness readings to a bias, where the bias is used to linearly interpolate + between ambient color temperature and + config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong. + This table is optional. If used, this array must, + 1) Contain at least two entries + 2) Be the same length as config_displayWhiteBalanceHighLightAmbientBiasesStrong. --> + <array name ="config_displayWhiteBalanceHighLightAmbientBrightnessesStrong"> + </array> + + <!-- See DisplayWhiteBalanceController. An array containing a list of biases. See config_displayWhiteBalanceHighLightAmbientBrightnesses for additional details. This array must be in the range of [0.0, 1.0]. --> @@ -5076,12 +5120,26 @@ </array> <!-- See DisplayWhiteBalanceController. + An array containing a list of biases. See + config_displayWhiteBalanceHighLightAmbientBrightnessesStrong for additional details. + This array must be in the range of [0.0, 1.0]. --> + <array name ="config_displayWhiteBalanceHighLightAmbientBiasesStrong"> + </array> + + <!-- See DisplayWhiteBalanceController. The ambient color temperature (in cct) to which we interpolate towards using the the look up table generated by config_displayWhiteBalanceHighLightAmbientBrightnesses and config_displayWhiteBalanceHighLightAmbientBiases. --> <item name="config_displayWhiteBalanceHighLightAmbientColorTemperature" format="float" type="dimen">8000.0</item> <!-- See DisplayWhiteBalanceController. + The ambient color temperature (in cct) to which we interpolate towards using the + the look up table generated by config_displayWhiteBalanceHighLightAmbientBrightnessesStrong + and config_displayWhiteBalanceHighLightAmbientBiasesStrong. Used when device is in Idle + Screen Brightness mode. --> + <item name="config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong" format="float" type="dimen">8000.0</item> + + <!-- See DisplayWhiteBalanceController. A float array containing a list of ambient color temperatures, in Kelvin. This array, together with config_displayWhiteBalanceDisplayColorTemperatures, is used to generate a lookup table used in DisplayWhiteBalanceController. This lookup table is used to map diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index c5aa8b05b948..0dd6c749b5a9 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1895,22 +1895,22 @@ <!-- Content description which should be used for the fingerprint icon. --> <string name="fingerprint_icon_content_description">Fingerprint icon</string> + <!-- Notification name shown when the system requires the user to set up device unlock. [CHAR LIMIT=NONE] --> + <string name="device_unlock_notification_name">Device unlock</string> + <!-- Notification title shown when the system suggests the user to set up another way to unlock. [CHAR LIMIT=NONE] --> + <string name="alternative_unlock_setup_notification_title">Try another way to unlock</string> + <!-- Notification content shown when the system suggests the user to enroll their face. [CHAR LIMIT=NONE] --> + <string name="alternative_face_setup_notification_content">Use Face Unlock when your fingerprint isn\'t recognized, like when your fingers are wet</string> + <!-- Notification content shown when the system suggests the user to enroll their fingerprint. [CHAR LIMIT=NONE] --> + <string name="alternative_fp_setup_notification_content">Use Fingerprint Unlock when your face isn\'t recognized, like when there\'s not enough light</string> <!-- Notification name shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] --> <string name="face_recalibrate_notification_name">Face Unlock</string> <!-- Notification title shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] --> <string name="face_recalibrate_notification_title">Issue with Face Unlock</string> <!-- Notification content shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] --> <string name="face_recalibrate_notification_content">Tap to delete your face model, then add your face again</string> - <!-- Title of a notification that directs the user to set up Face Unlock by enrolling their face. [CHAR LIMIT=NONE] --> - <string name="face_setup_notification_title">Set up Face Unlock</string> - <!-- Contents of a notification that directs the user to set up face unlock by enrolling their face. [CHAR LIMIT=NONE] --> - <string name="face_setup_notification_content">Unlock your phone by looking at it</string> <!-- Error message indicating that the camera privacy sensor has been turned on [CHAR LIMIT=NONE] --> <string name="face_sensor_privacy_enabled">To use Face Unlock, turn on <b>Camera access</b> in Settings > Privacy</string> - <!-- Title of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] --> - <string name="fingerprint_setup_notification_title">Set up more ways to unlock</string> - <!-- Contents of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] --> - <string name="fingerprint_setup_notification_content">Tap to add a fingerprint</string> <!-- Notification name shown when the system requires the user to re-calibrate their fingerprint. [CHAR LIMIT=NONE] --> <string name="fingerprint_recalibrate_notification_name">Fingerprint Unlock</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 851c0c2ce7c6..eaccd43d9fab 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2597,6 +2597,12 @@ <!-- Biometric FRR config --> <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" /> + <!-- Biometric FRR notification messages --> + <java-symbol type="string" name="device_unlock_notification_name" /> + <java-symbol type="string" name="alternative_unlock_setup_notification_title" /> + <java-symbol type="string" name="alternative_face_setup_notification_content" /> + <java-symbol type="string" name="alternative_fp_setup_notification_content" /> + <!-- Device credential strings for BiometricManager --> <java-symbol type="string" name="screen_lock_app_setting_name" /> <java-symbol type="string" name="screen_lock_dialog_default_subtitle" /> @@ -4167,11 +4173,17 @@ <java-symbol type="array" name="config_displayWhiteBalanceIncreaseThresholds" /> <java-symbol type="array" name="config_displayWhiteBalanceDecreaseThresholds" /> <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBrightnesses" /> + <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBrightnessesStrong" /> <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBiases" /> + <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBiasesStrong" /> <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientColorTemperature" /> + <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong" /> <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBrightnesses" /> + <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBrightnessesStrong" /> <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBiases" /> + <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBiasesStrong" /> <java-symbol type="dimen" name="config_displayWhiteBalanceHighLightAmbientColorTemperature" /> + <java-symbol type="dimen" name="config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong" /> <java-symbol type="array" name="config_displayWhiteBalanceAmbientColorTemperatures" /> <java-symbol type="array" name="config_displayWhiteBalanceDisplayColorTemperatures" /> <java-symbol type="array" name="config_displayWhiteBalanceStrongAmbientColorTemperatures" /> @@ -5185,6 +5197,7 @@ <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" /> <java-symbol type="drawable" name="focus_event_pressed_key_background" /> + <java-symbol type="drawable" name="focus_event_rotary_input_background" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index c14da299c6ef..7f56eb70b153 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -88,7 +88,7 @@ android_test { resource_dirs: ["res"], resource_zips: [":FrameworksCoreTests_apks_as_resources"], - java_resources: [":ApkVerityTestCertDer"], + java_resources: [":FrameworksCoreTests_unit_test_cert_der"], data: [ ":BinderDeathRecipientHelperApp1", diff --git a/core/tests/coretests/certs/Android.bp b/core/tests/coretests/certs/Android.bp index 8d4ecf4253c3..cefdc4dd60b1 100644 --- a/core/tests/coretests/certs/Android.bp +++ b/core/tests/coretests/certs/Android.bp @@ -13,3 +13,8 @@ android_app_certificate { name: "FrameworksCoreTests_unit_test_cert", certificate: "unit_test", } + +filegroup { + name: "FrameworksCoreTests_unit_test_cert_der", + srcs: ["unit_test.der"], +} diff --git a/core/tests/coretests/certs/README b/core/tests/coretests/certs/README index 00917a188934..b5c096e61a2e 100644 --- a/core/tests/coretests/certs/README +++ b/core/tests/coretests/certs/README @@ -2,3 +2,5 @@ Generate with: development/tools/make_key unit_test '/CN=unit_test' development/tools/make_key unit_test_diff '/CN=unit_test_diff' + +openssl x509 -in unit_test.x509.pem -out unit_test.der -outform der diff --git a/core/tests/coretests/certs/unit_test.der b/core/tests/coretests/certs/unit_test.der Binary files differnew file mode 100644 index 000000000000..4dbbc4946bda --- /dev/null +++ b/core/tests/coretests/certs/unit_test.der diff --git a/core/tests/coretests/res/raw/fsverity_sig b/core/tests/coretests/res/raw/fsverity_sig Binary files differindex b2f335dc0342..2c28f0be3cba 100644 --- a/core/tests/coretests/res/raw/fsverity_sig +++ b/core/tests/coretests/res/raw/fsverity_sig diff --git a/core/tests/coretests/src/android/security/keystore/OWNERS b/core/tests/coretests/src/android/security/keystore/OWNERS new file mode 100644 index 000000000000..d9e01161ce6d --- /dev/null +++ b/core/tests/coretests/src/android/security/keystore/OWNERS @@ -0,0 +1 @@ +include /keystore/OWNERS diff --git a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java index 151365481f20..a978e3b9e739 100644 --- a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java @@ -229,11 +229,12 @@ public class VerityUtilsTest { @Test public void testSignatureGeneratedExternally() throws Exception { var context = InstrumentationRegistry.getInstrumentation().getContext(); - byte[] cert = getClass().getClassLoader().getResourceAsStream("ApkVerityTestCert.der") + byte[] cert = getClass().getClassLoader().getResourceAsStream("unit_test.der") .readAllBytes(); // The signature is generated by: - // fsverity sign <(echo -n fs-verity) fsverity_sig --key=ApkVerityTestKey.pem \ - // --cert=ApkVerityTestCert.pem + // openssl pkcs8 -topk8 -nocrypt -in certs/unit_test.pk8 -out certs/unit_test.key.pem + // fsverity sign <(echo -n fs-verity) fsverity_sig --key=certs/unit_test.key.pem \ + // --cert=certs/unit_test.x509.pem byte[] sig = context.getResources().openRawResource(R.raw.fsverity_sig).readAllBytes(); // The fs-verity digest is generated by: // fsverity digest --compact <(echo -n fs-verity) diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java index 8be489ecd140..e8758754f059 100644 --- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java +++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java @@ -41,8 +41,6 @@ import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinit import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; -import androidx.test.InstrumentationRegistry; - import com.android.internal.R; import org.jetbrains.annotations.NotNull; @@ -838,31 +836,29 @@ public class VibrationEffectTest { @Test public void testAreVibrationFeaturesSupported_allSegmentsSupported() { - Vibrator vibrator = - createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) - .build()); + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build(); assertTrue(VibrationEffect.createWaveform( /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(VibrationEffect.createWaveform( /* timings= */ new long[] {1, 2, 3}, /* amplitudes= */ new int[] {10, 20, 40}, /* repeatIndex= */ 2) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue( VibrationEffect.startComposition() .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) .repeatEffectIndefinitely(TEST_ONE_SHOT) .compose() - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testAreVibrationFeaturesSupported_withUnsupportedSegments() { - Vibrator vibrator = - createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1).build()); + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build(); assertFalse( VibrationEffect.startComposition() @@ -872,7 +868,7 @@ public class VibrationEffectTest { /* amplitudes= */ new int[] {10, 20, 40}, /* repeatIndex= */ -1)) .compose() - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test @@ -996,13 +992,4 @@ public class VibrationEffectTest { return context; } - - private Vibrator createVibratorWithCustomInfo(VibratorInfo info) { - return new SystemVibrator(InstrumentationRegistry.getContext()) { - @Override - public VibratorInfo getInfo() { - return info; - } - }; - } } diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java index ff917aacba38..808c4ece9435 100644 --- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java +++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java @@ -57,6 +57,17 @@ public class VibratorInfoTest { } @Test + public void testHasFrequencyControl() { + VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); + assertFalse(noCapabilities.hasFrequencyControl()); + VibratorInfo composeAndFrequencyControl = new VibratorInfo.Builder(TEST_VIBRATOR_ID) + .setCapabilities( + IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS) + .build(); + assertTrue(composeAndFrequencyControl.hasFrequencyControl()); + } + + @Test public void testHasCapabilities() { VibratorInfo info = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) @@ -335,4 +346,186 @@ public class VibratorInfoTest { assertEquals(original, restored); assertEquals(original.hashCode(), restored.hashCode()); } + + @Test + public void areVibrationFeaturesSupported_noAmplitudeControl_fractionalAmplitudeUnsupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build(); + + // Have at least one fractional amplitude (amplitude not min (0) or max (255) or DEFAULT). + assertFalse(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30))); + assertFalse(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 255))); + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, /* amplitude= */ 40))); + } + + @Test + public void areVibrationFeaturesSupported_noAmplitudeControl_nonFractionalAmplitudeSupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1).build(); + + // All amplitudes are min, max, or default. Requires no amplitude control. + assertTrue(info.areVibrationFeaturesSupported( + waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, /* amplitude= */ 255))); + } + + @Test + public void areVibrationFeaturesSupported_withAmplitudeControl_allWaveformsSupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build(); + + // All forms of amplitudes are valid when amplitude control is available. + assertTrue(info.areVibrationFeaturesSupported( + waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1))); + assertTrue(info.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30, 50))); + assertTrue(info.areVibrationFeaturesSupported( + waveformWithAmplitudes(7, 255, 0, 0, 60))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, /* amplitude= */ 255))); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.createOneShot(20, /* amplitude= */ 40))); + } + + @Test + public void areVibrationFeaturesSupported_compositionsWithSupportedPrimitivesSupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose())); + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + /* scale= */ 0.2f, + /* delay= */ 200) + .compose())); + } + + @Test + public void areVibrationFeaturesSupported_compositionsWithUnupportedPrimitivesUnsupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) + .compose())); + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK) + .compose())); + } + + @Test + public void areVibrationFeaturesSupported_composedEffects_allComponentsSupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP) + .build(); + + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addEffect(VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, + /* amplitudes= */ new int[] {10, 20, 255}, + /* repeatIndex= */ -1)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) + .compose())); + + info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10) + .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_CLICK) + .build(); + + assertTrue(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) + .addEffect(VibrationEffect.createWaveform( + // These timings are given either 0 or default amplitudes, which + // do not require vibrator's amplitude control. + /* timings= */ new long[] {1, 2, 3}, + /* repeatIndex= */ -1)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + .compose())); + } + + @Test + public void areVibrationFeaturesSupported_composedEffects_someComponentsUnupported() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP) + .build(); + + // Not supported due to the TICK primitive, which the vibrator has no support for. + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) + .addEffect(VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, + /* amplitudes= */ new int[] {10, 20, 255}, + /* repeatIndex= */ -1)) + .compose())); + // Not supported due to the THUD effect, which the vibrator has no support for. + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addEffect(VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, + /* amplitudes= */ new int[] {10, 20, 255}, + /* repeatIndex= */ -1)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_THUD)) + .compose())); + + info = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10) + .setSupportedEffects(VibrationEffect.EFFECT_POP) + .build(); + + // Not supported due to fractional amplitudes (amplitudes not min (0) or max (255) or + // DEFAULT), because the vibrator has no amplitude control. + assertFalse(info.areVibrationFeaturesSupported( + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) + .addEffect(VibrationEffect.createWaveform( + /* timings= */ new long[] {1, 2, 3}, + /* amplitudes= */ new int[] {10, 20, 255}, + /* repeatIndex= */ -1)) + .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) + .compose())); + } + + private static VibrationEffect waveformWithAmplitudes(int...amplitudes) { + long[] timings = new long[amplitudes.length]; + for (int i = 0; i < timings.length; i++) { + timings[i] = i * 2; // Arbitrary timings. + } + return VibrationEffect.createWaveform(timings, amplitudes, /* repeatIndex= */ -1); + } } diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java index c559e34d92a3..8141ca4b22a8 100644 --- a/core/tests/vibrator/src/android/os/VibratorTest.java +++ b/core/tests/vibrator/src/android/os/VibratorTest.java @@ -578,189 +578,6 @@ public class VibratorTest { assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes); } - @Test - public void areVibrationFeaturesSupported_noAmplitudeControl_fractionalAmplitudes() { - Vibrator vibrator = - createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedEffects(VibrationEffect.EFFECT_THUD) - .build()); - - // Have at least one fractional amplitude (amplitude not min (0) or max (255) or DEFAULT). - assertFalse(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30))); - assertFalse(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 255))); - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, /* amplitude= */ 40))); - } - - @Test - public void areVibrationFeaturesSupported_noAmplitudeControl_nonFractionalAmplitudes() { - Vibrator vibrator = - createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedEffects(VibrationEffect.EFFECT_THUD) - .build()); - - // All amplitudes are min, max, or default. Requires no amplitude control. - assertTrue(vibrator.areVibrationFeaturesSupported( - waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, /* amplitude= */ 255))); - } - - @Test - public void areVibrationFeaturesSupported_withAmplitudeControl() { - Vibrator vibrator = - createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) - .build()); - - // All forms of amplitudes are valid when amplitude control is available. - assertTrue(vibrator.areVibrationFeaturesSupported( - waveformWithAmplitudes(255, 0, VibrationEffect.DEFAULT_AMPLITUDE, 255))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, /* repeatIndex= */ -1))); - assertTrue(vibrator.areVibrationFeaturesSupported(waveformWithAmplitudes(10, 30, 50))); - assertTrue(vibrator.areVibrationFeaturesSupported( - waveformWithAmplitudes(7, 255, 0, 0, 60))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, /* amplitude= */ 255))); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.createOneShot(20, /* amplitude= */ 40))); - } - - @Test - public void areVibrationFeaturesSupported_primitiveCompositionsWithSupportedPrimitives() { - Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .build()); - - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) - .compose())); - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - /* scale= */ 0.2f, - /* delay= */ 200) - .compose())); - } - - @Test - public void areVibrationFeaturesSupported_primitiveCompositionsWithUnupportedPrimitives() { - Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .build()); - - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) - .compose())); - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK) - .compose())); - } - - @Test - public void areVibrationFeaturesSupported_composedEffects_allComponentsSupported() { - Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP) - .build()); - - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) - .addEffect(VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, - /* amplitudes= */ new int[] {10, 20, 255}, - /* repeatIndex= */ -1)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) - .compose())); - - vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10) - .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_CLICK) - .build()); - - assertTrue(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) - .addEffect(VibrationEffect.createWaveform( - // These timings are given either 0 or default amplitudes, which - // do not require vibrator's amplitude control. - /* timings= */ new long[] {1, 2, 3}, - /* repeatIndex= */ -1)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) - .compose())); - } - - @Test - public void areVibrationFeaturesSupported_composedEffects_someComponentsUnupported() { - Vibrator vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_POP) - .build()); - - // Not supported due to the TICK primitive, which the vibrator has no support for. - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) - .addEffect(VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, - /* amplitudes= */ new int[] {10, 20, 255}, - /* repeatIndex= */ -1)) - .compose())); - // Not supported due to the THUD effect, which the vibrator has no support for. - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) - .addEffect(VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, - /* amplitudes= */ new int[] {10, 20, 255}, - /* repeatIndex= */ -1)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_THUD)) - .compose())); - - vibrator = createVibratorWithCustomInfo(new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 10) - .setSupportedEffects(VibrationEffect.EFFECT_POP) - .build()); - - // Not supported due to fractional amplitudes (amplitudes not min (0) or max (255) or - // DEFAULT), because the vibrator has no amplitude control. - assertFalse(vibrator.areVibrationFeaturesSupported( - VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD) - .addEffect(VibrationEffect.createWaveform( - /* timings= */ new long[] {1, 2, 3}, - /* amplitudes= */ new int[] {10, 20, 255}, - /* repeatIndex= */ -1)) - .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_POP)) - .compose())); - } - /** * Asserts that the frequency profile is empty, and therefore frequency control isn't supported. */ @@ -768,21 +585,4 @@ public class VibratorTest { assertTrue(info.getFrequencyProfile().isEmpty()); assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); } - - private Vibrator createVibratorWithCustomInfo(VibratorInfo info) { - return new SystemVibrator(mContextSpy) { - @Override - public VibratorInfo getInfo() { - return info; - } - }; - } - - private static VibrationEffect waveformWithAmplitudes(int...amplitudes) { - long[] timings = new long[amplitudes.length]; - for (int i = 0; i < timings.length; i++) { - timings[i] = i * 2; // Arbitrary timings. - } - return VibrationEffect.createWaveform(timings, amplitudes, /* repeatIndex= */ -1); - } } diff --git a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java index 8268077e72e9..4f5f3c0ddeaf 100644 --- a/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/PrebakedSegmentTest.java @@ -25,18 +25,14 @@ import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertThrows; import android.os.Parcel; -import android.os.SystemVibrator; import android.os.VibrationEffect; -import android.os.Vibrator; import android.os.VibratorInfo; -import androidx.test.InstrumentationRegistry; - import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -@RunWith(MockitoJUnitRunner.class) +@RunWith(JUnit4.class) public class PrebakedSegmentTest { @Test @@ -149,121 +145,121 @@ public class PrebakedSegmentTest { @Test public void testVibrationFeaturesSupport_idsWithFallback_fallbackEnabled_vibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects( + VibratorInfo info = createVibratorInfoWithSupportedEffects( VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_DOUBLE_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithFallback_fallbackEnabled_noVibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]); + VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_HEAVY_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithFallback_fallbackDisabled_vibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects( + VibratorInfo info = createVibratorInfoWithSupportedEffects( VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_DOUBLE_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_HEAVY_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithFallback_fallbackDisabled_noVibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]); + VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_DOUBLE_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_HEAVY_CLICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackEnabled_vibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects( + VibratorInfo info = createVibratorInfoWithSupportedEffects( VibrationEffect.EFFECT_THUD, VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_TEXTURE_TICK); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_THUD) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_POP) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackEnabled_noVibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]); + VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]); assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_THUD) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_POP) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithFallback(VibrationEffect.EFFECT_TEXTURE_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackDisabled_vibratorSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects( + VibratorInfo info = createVibratorInfoWithSupportedEffects( VibrationEffect.EFFECT_THUD, VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_TEXTURE_TICK); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_THUD) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_POP) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertTrue(createSegmentWithoutFallback(VibrationEffect.EFFECT_TEXTURE_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_idsWithNoFallback_fallbackDisabled_noVibSupport() { - Vibrator vibrator = createVibratorWithSupportedEffects(new int[0]); + VibratorInfo info = createVibratorInfoWithSupportedEffects(new int[0]); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_THUD) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_POP) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); assertFalse(createSegmentWithoutFallback(VibrationEffect.EFFECT_TEXTURE_TICK) - .areVibrationFeaturesSupported(vibrator)); + .areVibrationFeaturesSupported(info)); } @Test @@ -283,14 +279,9 @@ public class PrebakedSegmentTest { return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM); } - private static Vibrator createVibratorWithSupportedEffects(int... supportedEffects) { - return new SystemVibrator(InstrumentationRegistry.getContext()) { - @Override - public VibratorInfo getInfo() { - return new VibratorInfo.Builder(/* id= */ 1) - .setSupportedEffects(supportedEffects) - .build(); - } - }; + private static VibratorInfo createVibratorInfoWithSupportedEffects(int... supportedEffects) { + return new VibratorInfo.Builder(/* id= */ 1) + .setSupportedEffects(supportedEffects) + .build(); } } diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java index 6f5adcd26ba5..ec5a084c2ddb 100644 --- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java @@ -25,18 +25,14 @@ import static org.testng.Assert.assertThrows; import android.hardware.vibrator.IVibrator; import android.os.Parcel; -import android.os.SystemVibrator; import android.os.VibrationEffect; -import android.os.Vibrator; import android.os.VibratorInfo; -import androidx.test.InstrumentationRegistry; - import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -@RunWith(MockitoJUnitRunner.class) +@RunWith(JUnit4.class) public class PrimitiveSegmentTest { private static final float TOLERANCE = 1e-2f; @@ -146,15 +142,15 @@ public class PrimitiveSegmentTest { public void testVibrationFeaturesSupport_primitiveSupportedByVibrator() { assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK))); assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD))); assertTrue(createSegment(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_QUICK_RISE))); } @@ -162,15 +158,15 @@ public class PrimitiveSegmentTest { public void testVibrationFeaturesSupport_primitiveNotSupportedByVibrator() { assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_CLICK) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD))); assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK))); assertFalse(createSegment(VibrationEffect.Composition.PRIMITIVE_THUD) .areVibrationFeaturesSupported( - createVibratorWithSupportedPrimitive( + createVibratorInfoWithSupportedPrimitive( VibrationEffect.Composition.PRIMITIVE_QUICK_RISE))); } @@ -193,15 +189,10 @@ public class PrimitiveSegmentTest { return new PrimitiveSegment(primitiveId, 0.2f, 10); } - private static Vibrator createVibratorWithSupportedPrimitive(int primitiveId) { - return new SystemVibrator(InstrumentationRegistry.getContext()) { - @Override - public VibratorInfo getInfo() { - return new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(primitiveId, 10) - .build(); - } - }; + private static VibratorInfo createVibratorInfoWithSupportedPrimitive(int primitiveId) { + return new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(primitiveId, 10) + .build(); } } diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java index 68870e5a2979..5caa86bb9fb5 100644 --- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java @@ -23,31 +23,21 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.junit.MockitoRule; +import org.junit.runners.JUnit4; -@RunWith(MockitoJUnitRunner.class) +@RunWith(JUnit4.class) public class RampSegmentTest { private static final float TOLERANCE = 1e-2f; - @Rule - public MockitoRule mMockitoRule = MockitoJUnit.rule(); - - @Mock - private Vibrator mVibrator; - @Test public void testCreation() { RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0, @@ -147,71 +137,71 @@ public class RampSegmentTest { @Test public void testVibrationFeaturesSupport_amplitudeAndFrequencyControls_supported() { - when(mVibrator.hasAmplitudeControl()).thenReturn(true); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); // Increasing amplitude - assertTrue(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); + assertTrue(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(info)); // Increasing frequency - assertTrue(new RampSegment(0.5f, 0.5f, 0, 1, 10).areVibrationFeaturesSupported(mVibrator)); + assertTrue(new RampSegment(0.5f, 0.5f, 0, 1, 10).areVibrationFeaturesSupported(info)); // Decreasing amplitude - assertTrue(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); + assertTrue(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(info)); // Decreasing frequency - assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 10).areVibrationFeaturesSupported(mVibrator)); + assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 10).areVibrationFeaturesSupported(info)); // Zero duration - assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 0).areVibrationFeaturesSupported(mVibrator)); + assertTrue(new RampSegment(0.5f, 0.5f, 1, 0, 0).areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_noAmplitudeControl_unsupportedForChangingAmplitude() { - when(mVibrator.hasAmplitudeControl()).thenReturn(false); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true); // Test with increasing/decreasing amplitudes. - assertFalse(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); + assertFalse(new RampSegment(0.5f, 1, 0, 0, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(1, 0.5f, 0, 0, 10).areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_noAmplitudeControl_fractionalAmplitudeUnsupported() { - when(mVibrator.hasAmplitudeControl()).thenReturn(false); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true); - assertFalse(new RampSegment(0.2f, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(0, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(0.2f, 0, 0, 0, 10).areVibrationFeaturesSupported(mVibrator)); + assertFalse(new RampSegment(0.2f, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(0, 0.2f, 0, 0, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(0.2f, 0, 0, 0, 10).areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_unchangingZeroAmplitude_supported() { RampSegment amplitudeZeroWithIncreasingFrequency = new RampSegment(1, 1, 0.5f, 0.8f, 10); RampSegment amplitudeZeroWithDecreasingFrequency = new RampSegment(1, 1, 0.8f, 0.5f, 10); - when(mVibrator.hasFrequencyControl()).thenReturn(true); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true); - assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); - assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(amplitudeZeroWithIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(amplitudeZeroWithDecreasingFrequency.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_unchangingOneAmplitude_supported() { RampSegment amplitudeOneWithIncreasingFrequency = new RampSegment(1, 1, 0.5f, 0.8f, 10); RampSegment amplitudeOneWithDecreasingFrequency = new RampSegment(1, 1, 0.8f, 0.5f, 10); - when(mVibrator.hasFrequencyControl()).thenReturn(true); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true); - assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); - assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(amplitudeOneWithIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(amplitudeOneWithDecreasingFrequency.areVibrationFeaturesSupported(info)); } @Test @@ -220,52 +210,52 @@ public class RampSegmentTest { new RampSegment(DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, 0.5f, 0.8f, 10); RampSegment defaultAmplitudeDecreasingFrequency = new RampSegment(DEFAULT_AMPLITUDE, DEFAULT_AMPLITUDE, 0.8f, 0.5f, 10); - when(mVibrator.hasFrequencyControl()).thenReturn(true); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ false, /* hasFrequencyControl= */ true); - assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); - assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(mVibrator)); - assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(mVibrator)); + assertTrue(defaultAmplitudeIncreasingFrequency.areVibrationFeaturesSupported(info)); + assertTrue(defaultAmplitudeDecreasingFrequency.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_noFrequencyControl_unsupportedForChangingFrequency() { - when(mVibrator.hasAmplitudeControl()).thenReturn(true); - when(mVibrator.hasFrequencyControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false); // Test with increasing/decreasing frequencies. - assertFalse(new RampSegment(0, 0, 0.2f, 0.4f, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(0, 0, 0.4f, 0.2f, 10).areVibrationFeaturesSupported(mVibrator)); + assertFalse(new RampSegment(0, 0, 0.2f, 0.4f, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(0, 0, 0.4f, 0.2f, 10).areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_noFrequencyControl_fractionalFrequencyUnsupported() { - when(mVibrator.hasAmplitudeControl()).thenReturn(true); - when(mVibrator.hasFrequencyControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false); - assertFalse(new RampSegment(0, 0, 0.2f, 0.2f, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(0, 0, 0.2f, 0, 10).areVibrationFeaturesSupported(mVibrator)); - assertFalse(new RampSegment(0, 0, 0, 0.2f, 10).areVibrationFeaturesSupported(mVibrator)); + assertFalse(new RampSegment(0, 0, 0.2f, 0.2f, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(0, 0, 0.2f, 0, 10).areVibrationFeaturesSupported(info)); + assertFalse(new RampSegment(0, 0, 0, 0.2f, 10).areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_unchangingZeroFrequency_supported() { RampSegment frequencyZeroWithIncreasingAmplitude = new RampSegment(0.1f, 1, 0, 0, 10); RampSegment frequencyZeroWithDecreasingAmplitude = new RampSegment(1, 0.1f, 0, 0, 10); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); - when(mVibrator.hasFrequencyControl()).thenReturn(false); + VibratorInfo info = + createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ false); - assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(mVibrator)); - assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(mVibrator)); + assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(info)); + assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(info)); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + info = createVibInfo(/* hasAmplitudeControl= */ true, /* hasFrequencyControl= */ true); - assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(mVibrator)); - assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(mVibrator)); + assertTrue(frequencyZeroWithIncreasingAmplitude.areVibrationFeaturesSupported(info)); + assertTrue(frequencyZeroWithDecreasingAmplitude.areVibrationFeaturesSupported(info)); } @Test @@ -274,4 +264,17 @@ public class RampSegmentTest { // duration checked in VibrationEffect implementations. assertTrue(new RampSegment(0.5f, 1, 0, 0, 5_000).isHapticFeedbackCandidate()); } + + private static VibratorInfo createVibInfo( + boolean hasAmplitudeControl, boolean hasFrequencyControl) { + VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1); + long capabilities = 0; + if (hasAmplitudeControl) { + capabilities |= IVibrator.CAP_AMPLITUDE_CONTROL; + } + if (hasFrequencyControl) { + capabilities |= (IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + } + return builder.setCapabilities(capabilities).build(); + } } diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java index 34bb892b07d3..44db30603089 100644 --- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java @@ -21,31 +21,20 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; -import android.os.Vibrator; +import android.os.VibratorInfo; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.junit.MockitoRule; +import org.junit.runners.JUnit4; -@RunWith(MockitoJUnitRunner.class) +@RunWith(JUnit4.class) public class StepSegmentTest { private static final float TOLERANCE = 1e-2f; - - @Rule - public MockitoRule mMockitoRule = MockitoJUnit.rule(); - - @Mock - private Vibrator mVibrator; - @Test public void testCreation() { StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequencyHz= */ 1f, @@ -160,26 +149,26 @@ public class StepSegmentTest { public void testVibrationFeaturesSupport_zeroAmplitude_supported() { StepSegment segment = new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, /* duration= */ 0); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_maxAmplitude_supported() { StepSegment segment = new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 0, /* duration= */ 0); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); } @Test @@ -189,60 +178,60 @@ public class StepSegmentTest { /* amplitude= */ VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, /* duration= */ 0); - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_fractionalAmplitude_hasAmplitudeCtrl_supported() { - when(mVibrator.hasAmplitudeControl()).thenReturn(true); + VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ true); assertTrue(new StepSegment(/* amplitude= */ 0.2f, /* frequencyHz= */ 0, /* duration= */ 0) - .areVibrationFeaturesSupported(mVibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_fractionalAmplitude_hasNoAmplitudeCtrl_notSupported() { - when(mVibrator.hasAmplitudeControl()).thenReturn(false); + VibratorInfo info = createVibInfoForAmplitude(/* hasAmplitudeControl= */ false); assertFalse(new StepSegment(/* amplitude= */ 0.2f, /* frequencyHz= */ 0, /* duration= */ 0) - .areVibrationFeaturesSupported(mVibrator)); + .areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_zeroFrequency_supported() { StepSegment segment = new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0, /* duration= */ 0); - when(mVibrator.hasFrequencyControl()).thenReturn(false); + VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ false); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + info = createVibInfoForFrequency(/* hasFrequencyControl= */ true); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_nonZeroFrequency_hasFrequencyCtrl_supported() { StepSegment segment = new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0.2f, /* duration= */ 0); - when(mVibrator.hasFrequencyControl()).thenReturn(true); + VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ true); - assertTrue(segment.areVibrationFeaturesSupported(mVibrator)); + assertTrue(segment.areVibrationFeaturesSupported(info)); } @Test public void testVibrationFeaturesSupport_nonZeroFrequency_hasNoFrequencyCtrl_notSupported() { StepSegment segment = new StepSegment(/* amplitude= */ 0f, /* frequencyHz= */ 0.2f, /* duration= */ 0); - when(mVibrator.hasFrequencyControl()).thenReturn(false); + VibratorInfo info = createVibInfoForFrequency(/* hasFrequencyControl= */ false); - assertFalse(segment.areVibrationFeaturesSupported(mVibrator)); + assertFalse(segment.areVibrationFeaturesSupported(info)); } @Test @@ -251,4 +240,21 @@ public class StepSegmentTest { // duration checked in VibrationEffect implementations. assertTrue(new StepSegment(0, 0, 5_000).isHapticFeedbackCandidate()); } + + private static VibratorInfo createVibInfoForAmplitude(boolean hasAmplitudeControl) { + VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1); + if (hasAmplitudeControl) { + builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + } + return builder.build(); + } + + private static VibratorInfo createVibInfoForFrequency(boolean hasFrequencyControl) { + VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1); + if (hasFrequencyControl) { + builder.setCapabilities( + IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + } + return builder.build(); + } } diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 98629a24696a..36bfb98e726b 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -235,6 +235,22 @@ public final class SystemFonts { } /** + * Get the updated FontConfig. + * + * @param updatableFontMap a font mapping of updated font files. + * @hide + */ + public static @NonNull FontConfig getSystemFontConfigForTesting( + @NonNull String fontsXml, + @Nullable Map<String, File> updatableFontMap, + long lastModifiedDate, + int configVersion + ) { + return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, + updatableFontMap, lastModifiedDate, configVersion); + } + + /** * Get the system preinstalled FontConfig. * @hide */ diff --git a/keystore/OWNERS b/keystore/OWNERS index 7ab9d761e236..913f65586cd6 100644 --- a/keystore/OWNERS +++ b/keystore/OWNERS @@ -1,4 +1,4 @@ +# Bug component: 189335 eranm@google.com jbires@google.com -jdanis@google.com swillden@google.com diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java index 54955c6b7fab..1394bd443f03 100644 --- a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java +++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java @@ -325,32 +325,25 @@ public abstract class KeyStore2ParameterUtils { args.add(KeyStore2ParameterUtils.makeBool( KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED)); } else { - if (spec.getUserAuthenticationValidityDurationSeconds() == 0) { - // Every use of this key needs to be authorized by the user. - addSids(args, spec); - args.add(KeyStore2ParameterUtils.makeEnum( - KeymasterDefs.KM_TAG_USER_AUTH_TYPE, spec.getUserAuthenticationType() + addSids(args, spec); + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_USER_AUTH_TYPE, spec.getUserAuthenticationType() + )); + if (spec.getUserAuthenticationValidityDurationSeconds() != 0) { + args.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_AUTH_TIMEOUT, + spec.getUserAuthenticationValidityDurationSeconds() )); - - if (spec.isUserAuthenticationValidWhileOnBody()) { + } + if (spec.isUserAuthenticationValidWhileOnBody()) { + if (spec.getUserAuthenticationValidityDurationSeconds() == 0) { throw new ProviderException( "Key validity extension while device is on-body is not " + "supported for keys requiring fingerprint authentication"); } - } else { - addSids(args, spec); - args.add(KeyStore2ParameterUtils.makeEnum( - KeymasterDefs.KM_TAG_USER_AUTH_TYPE, spec.getUserAuthenticationType() + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY )); - args.add(KeyStore2ParameterUtils.makeInt( - KeymasterDefs.KM_TAG_AUTH_TIMEOUT, - spec.getUserAuthenticationValidityDurationSeconds() - )); - if (spec.isUserAuthenticationValidWhileOnBody()) { - args.add(KeyStore2ParameterUtils.makeBool( - KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY - )); - } } } } diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml new file mode 100644 index 000000000000..6e4752c9d27d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + <path + android:fillColor="@color/compat_controls_background" + android:strokeAlpha="0.8" + android:fillAlpha="0.8" + android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/> + <group + android:translateX="12" + android:translateY="12"> + <path + android:fillColor="@color/compat_controls_text" + android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml new file mode 100644 index 000000000000..141a1ce60b8e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/compat_background_ripple"> + <item android:drawable="@drawable/user_aspect_ratio_settings_button"/> +</ripple>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index dfaeeeb81c07..257fe1544bbb 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -55,7 +55,7 @@ <include android:id="@+id/size_compat_hint" android:visibility="gone" - android:layout_width="@dimen/size_compat_hint_width" + android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml new file mode 100644 index 000000000000..433d8546ece0 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.compatui.UserAspectRatioSettingsLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="bottom|end"> + + <include android:id="@+id/user_aspect_ratio_settings_hint" + android:visibility="gone" + android:layout_width="@dimen/compat_hint_width" + android:layout_height="wrap_content" + layout="@layout/compat_mode_hint"/> + + <ImageButton + android:id="@+id/user_aspect_ratio_settings_button" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/compat_button_margin" + android:layout_marginBottom="@dimen/compat_button_margin" + android:src="@drawable/user_aspect_ratio_settings_button_ripple" + android:background="@android:color/transparent" + android:contentDescription="@string/user_aspect_ratio_settings_button_description"/> + +</com.android.wm.shell.compatui.UserAspectRatioSettingsLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index ac73e1d87ba2..597e899d098d 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -260,8 +260,8 @@ + compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). --> <dimen name="compat_hint_padding_end">7dp</dimen> - <!-- The width of the size compat hint. --> - <dimen name="size_compat_hint_width">188dp</dimen> + <!-- The width of the compat hint. --> + <dimen name="compat_hint_width">188dp</dimen> <!-- The width of the camera compat hint. --> <dimen name="camera_compat_hint_width">143dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index b192fdf245e2..8cbc3d016b01 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -173,7 +173,13 @@ <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> - <string name="restart_button_description">Tap to restart this app for a better view.</string> + <string name="restart_button_description">Tap to restart this app for a better view</string> + + <!-- Tooltip text of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] --> + <string name="user_aspect_ratio_settings_button_hint">Change this app\'s aspect ratio in Settings</string> + + <!-- Content description of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] --> + <string name="user_aspect_ratio_settings_button_description">Change aspect ratio</string> <!-- Description of the camera compat button for applying stretched issues treatment in the hint for compatibility control. [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 0998e7134e00..54f89846ac85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -16,12 +16,17 @@ package com.android.wm.shell.compatui; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.hardware.display.DisplayManager; +import android.provider.Settings; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -41,7 +46,6 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -104,6 +108,13 @@ public class CompatUIController implements OnDisplaysChangedListener, private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>(); /** + * The active user aspect ratio settings button layout if there is one (there can be at most + * one active). + */ + @Nullable + private UserAspectRatioSettingsWindowManager mUserAspectRatioSettingsLayout; + + /** * The active Letterbox Education layout if there is one (there can be at most one active). * * <p>An active layout is a layout that is eligible to be shown for the associated task but @@ -121,38 +132,51 @@ public class CompatUIController implements OnDisplaysChangedListener, /** Avoid creating display context frequently for non-default display. */ private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); + @NonNull private final Context mContext; + @NonNull private final ShellController mShellController; + @NonNull private final DisplayController mDisplayController; + @NonNull private final DisplayInsetsController mDisplayInsetsController; + @NonNull private final DisplayImeController mImeController; + @NonNull private final SyncTransactionQueue mSyncQueue; + @NonNull private final ShellExecutor mMainExecutor; + @NonNull private final Lazy<Transitions> mTransitionsLazy; + @NonNull private final DockStateReader mDockStateReader; + @NonNull private final CompatUIConfiguration mCompatUIConfiguration; // Only show each hint once automatically in the process life. + @NonNull private final CompatUIHintsState mCompatUIHintsState; + @NonNull private final CompatUIShellCommandHandler mCompatUIShellCommandHandler; - private CompatUICallback mCallback; + @Nullable + private CompatUICallback mCompatUICallback; // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't // be shown. private boolean mKeyguardShowing; - public CompatUIController(Context context, - ShellInit shellInit, - ShellController shellController, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - DisplayImeController imeController, - SyncTransactionQueue syncQueue, - ShellExecutor mainExecutor, - Lazy<Transitions> transitionsLazy, - DockStateReader dockStateReader, - CompatUIConfiguration compatUIConfiguration, - CompatUIShellCommandHandler compatUIShellCommandHandler) { + public CompatUIController(@NonNull Context context, + @NonNull ShellInit shellInit, + @NonNull ShellController shellController, + @NonNull DisplayController displayController, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull DisplayImeController imeController, + @NonNull SyncTransactionQueue syncQueue, + @NonNull ShellExecutor mainExecutor, + @NonNull Lazy<Transitions> transitionsLazy, + @NonNull DockStateReader dockStateReader, + @NonNull CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -175,9 +199,9 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler.onInit(); } - /** Sets the callback for UI interactions. */ - public void setCompatUICallback(CompatUICallback callback) { - mCallback = callback; + /** Sets the callback for Compat UI interactions. */ + public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) { + mCompatUICallback = compatUiCallback; } /** @@ -187,7 +211,7 @@ public class CompatUIController implements OnDisplaysChangedListener, * @param taskInfo {@link TaskInfo} task the activity is in. * @param taskListener listener to handle the Task Surface placement. */ - public void onCompatInfoChanged(TaskInfo taskInfo, + public void onCompatInfoChanged(@NonNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (taskInfo != null && !taskInfo.topActivityInSizeCompat) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); @@ -203,6 +227,16 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + // The user aspect ratio button should not be handled when a new TaskInfo is + // sent because of a double tap or when in multi-window mode. + if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + return; + } + if (!taskInfo.isFromLetterboxDoubleTap) { + createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); + } } } @@ -280,8 +314,8 @@ public class CompatUIController implements OnDisplaysChangedListener, return mDisplaysWithIme.contains(displayId); } - private void createOrUpdateCompatLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId); if (layout != null) { if (layout.needsToBeRecreated(taskInfo, taskListener)) { @@ -314,7 +348,7 @@ public class CompatUIController implements OnDisplaysChangedListener, CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { return new CompatUIWindowManager(context, - taskInfo, mSyncQueue, mCallback, taskListener, + taskInfo, mSyncQueue, mCompatUICallback, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, mCompatUIConfiguration, this::onRestartButtonClicked); } @@ -328,12 +362,12 @@ public class CompatUIController implements OnDisplaysChangedListener, mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); onCompatInfoChanged(taskInfoState.first, taskInfoState.second); } else { - mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); + mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); } } - private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (mActiveLetterboxEduLayout != null) { if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) { mActiveLetterboxEduLayout.release(); @@ -377,8 +411,8 @@ public class CompatUIController implements OnDisplaysChangedListener, mDockStateReader, mCompatUIConfiguration); } - private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { RestartDialogWindowManager layout = mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); if (layout != null) { @@ -423,7 +457,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onRestartDialogCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); - mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); + mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); } private void onRestartDialogDismissCallback( @@ -432,8 +466,8 @@ public class CompatUIController implements OnDisplaysChangedListener, onCompatInfoChanged(stateInfo.first, stateInfo.second); } - private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (mActiveReachabilityEduLayout != null) { if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) { mActiveReachabilityEduLayout.release(); @@ -474,14 +508,67 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), - mCompatUIConfiguration, mMainExecutor); + mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed); } + private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, + @NonNull ShellTaskOrganizer.TaskListener taskListener) { + // We need to update the UI otherwise it will not be shown until the user relaunches the app + createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); + } + + private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + if (mUserAspectRatioSettingsLayout != null) { + if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } else { + // UI already exists, update the UI layout. + if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } + return; + } + } + + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + final UserAspectRatioSettingsWindowManager newLayout = + createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener); + if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, add it the active layouts. + mUserAspectRatioSettingsLayout = newLayout; + } + } + + @VisibleForTesting + @NonNull + UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager( + @NonNull Context context, @NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue, + taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), + mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor); + } + + private void launchUserAspectRatioSettings( + @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { + final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivity(intent); + } private void removeLayouts(int taskId) { - final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); - if (layout != null) { - layout.release(); + final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); + if (compatLayout != null) { + compatLayout.release(); mActiveCompatLayouts.remove(taskId); } @@ -502,6 +589,12 @@ public class CompatUIController implements OnDisplaysChangedListener, mActiveReachabilityEduLayout.release(); mActiveReachabilityEduLayout = null; } + + if (mUserAspectRatioSettingsLayout != null + && mUserAspectRatioSettingsLayout.getTaskId() == taskId) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } } private Context getOrCreateDisplayContext(int displayId) { @@ -557,6 +650,10 @@ public class CompatUIController implements OnDisplaysChangedListener, if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { callback.accept(mActiveReachabilityEduLayout); } + if (mUserAspectRatioSettingsLayout != null && condition.test( + mUserAspectRatioSettingsLayout)) { + callback.accept(mUserAspectRatioSettingsLayout); + } } /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ @@ -591,4 +688,14 @@ public class CompatUIController implements OnDisplaysChangedListener, insetsChanged(insetsState); } } + + /** + * A class holding the state of the compat UI hints, which is shared between all compat UI + * window managers. + */ + static class CompatUIHintsState { + boolean mHasShownSizeCompatHint; + boolean mHasShownCameraCompatHint; + boolean mHasShownUserAspectRatioSettingsButtonHint; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 065806df3dc8..ce3c5093fdd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -38,6 +38,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import java.util.function.Consumer; @@ -235,15 +236,4 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; } - - /** - * A class holding the state of the compat UI hints, which is shared between all compat UI - * window managers. - */ - static class CompatUIHintsState { - @VisibleForTesting - boolean mHasShownSizeCompatHint; - @VisibleForTesting - boolean mHasShownCameraCompatHint; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 95bb1fe1c986..9de3f9dec34e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -36,6 +36,8 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import java.util.function.BiConsumer; + /** * Window manager for the reachability education */ @@ -73,6 +75,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { // we need to animate them. private boolean mHasLetterboxSizeChanged; + private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback; + @Nullable @VisibleForTesting ReachabilityEduLayout mLayout; @@ -80,7 +84,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { ReachabilityEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, - CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) { + CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor, + BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onDismissCallback) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; @@ -89,6 +94,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; mCompatUIConfiguration = compatUIConfiguration; mMainExecutor = mainExecutor; + mOnDismissCallback = onDismissCallback; } @Override @@ -217,13 +223,17 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { return; } final TaskInfo lastTaskInfo = getLastTaskInfo(); + final boolean hasSeenHorizontalReachabilityEdu = + mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo); + final boolean hasSeenVerticalReachabilityEdu = + mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo); final boolean eligibleForDisplayHorizontalEducation = mForceUpdate - || !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo) + || !hasSeenHorizontalReachabilityEdu || (mHasUserDoubleTapped && (mLetterboxHorizontalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxHorizontalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); final boolean eligibleForDisplayVerticalEducation = mForceUpdate - || !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo) + || !hasSeenVerticalReachabilityEdu || (mHasUserDoubleTapped && (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); @@ -239,6 +249,14 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { if (!mHasLetterboxSizeChanged) { updateHideTime(); mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS); + // If reachability education has been seen for the first time, trigger callback to + // display aspect ratio settings button once reachability education disappears + if (hasShownHorizontalReachabilityEduFirstTime(hasSeenHorizontalReachabilityEdu) + || hasShownVerticalReachabilityEduFirstTime( + hasSeenVerticalReachabilityEdu)) { + mMainExecutor.executeDelayed(this::triggerOnDismissCallback, + DISAPPEAR_DELAY_MS); + } } mHasUserDoubleTapped = false; } else { @@ -246,6 +264,38 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { } } + /** + * Compares the value of + * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} before and after the + * layout is shown. Horizontal reachability education is considered seen for the first time if + * prior to viewing the layout, + * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} is {@code false} + * but becomes {@code true} once the current layout is shown. + */ + private boolean hasShownHorizontalReachabilityEduFirstTime( + boolean previouslyShownHorizontalReachabilityEducation) { + return !previouslyShownHorizontalReachabilityEducation + && mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(getLastTaskInfo()); + } + + /** + * Compares the value of + * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} before and after the + * layout is shown. Horizontal reachability education is considered seen for the first time if + * prior to viewing the layout, + * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} is {@code false} + * but becomes {@code true} once the current layout is shown. + */ + private boolean hasShownVerticalReachabilityEduFirstTime( + boolean previouslyShownVerticalReachabilityEducation) { + return !previouslyShownVerticalReachabilityEducation + && mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(getLastTaskInfo()); + } + + private void triggerOnDismissCallback() { + mOnDismissCallback.accept(getLastTaskInfo(), getTaskListener()); + } + private void hideReachability() { if (mLayout == null || !shouldHideEducation()) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java new file mode 100644 index 000000000000..5eeb3b650074 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.IdRes; +import android.annotation.NonNull; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * Layout for the user aspect ratio button which opens the app list page in settings + * and allows users to change apps aspect ratio. + */ +public class UserAspectRatioSettingsLayout extends LinearLayout { + + private static final float ALPHA_FULL_TRANSPARENT = 0f; + + private static final float ALPHA_FULL_OPAQUE = 1f; + + private static final long VISIBILITY_ANIMATION_DURATION_MS = 50; + + private static final String ALPHA_PROPERTY_NAME = "alpha"; + + private UserAspectRatioSettingsWindowManager mWindowManager; + + public UserAspectRatioSettingsLayout(Context context) { + this(context, null); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(@NonNull UserAspectRatioSettingsWindowManager windowManager) { + mWindowManager = windowManager; + } + + void setUserAspectRatioSettingsHintVisibility(boolean show) { + setViewVisibility(R.id.user_aspect_ratio_settings_hint, show); + } + + void setUserAspectRatioButtonVisibility(boolean show) { + setViewVisibility(R.id.user_aspect_ratio_settings_button, show); + // Hint should never be visible without button. + if (!show) { + setUserAspectRatioSettingsHintVisibility(/* show= */ false); + } + } + + private void setViewVisibility(@IdRes int resId, boolean show) { + final View view = findViewById(resId); + int visibility = show ? View.VISIBLE : View.GONE; + if (view.getVisibility() == visibility) { + return; + } + if (show) { + showItem(view); + } else { + view.setVisibility(visibility); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + // Need to relayout after changes like hiding / showing a hint since they affect size. + // Doing this directly in setUserAspectRatioButtonVisibility can result in flaky animation. + mWindowManager.relayout(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final ImageButton userAspectRatioButton = + findViewById(R.id.user_aspect_ratio_settings_button); + userAspectRatioButton.setOnClickListener( + view -> mWindowManager.onUserAspectRatioSettingsButtonClicked()); + userAspectRatioButton.setOnLongClickListener(view -> { + mWindowManager.onUserAspectRatioSettingsButtonLongClicked(); + return true; + }); + + final LinearLayout sizeCompatHint = findViewById(R.id.user_aspect_ratio_settings_hint); + ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text)) + .setText(R.string.user_aspect_ratio_settings_button_hint); + sizeCompatHint.setOnClickListener( + view -> setUserAspectRatioSettingsHintVisibility(/* show= */ false)); + } + + private void showItem(@NonNull View view) { + view.setVisibility(View.VISIBLE); + final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME, + ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE); + fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS); + fadeIn.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.VISIBLE); + } + }); + fadeIn.start(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java new file mode 100644 index 000000000000..bd53dc7390c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; + +import java.util.function.BiConsumer; + +/** + * Window manager for the user aspect ratio settings button which allows users to go to + * app settings and change apps aspect ratio. + */ +class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract { + + private static final long SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 500L; + + private static final long HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 4000L; + + private long mNextButtonHideTimeMs = -1L; + + private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnButtonClicked; + + private final ShellExecutor mShellExecutor; + + @VisibleForTesting + @NonNull + final CompatUIHintsState mCompatUIHintsState; + + @Nullable + private UserAspectRatioSettingsLayout mLayout; + + // Remember the last reported states in case visibility changes due to keyguard or IME updates. + @VisibleForTesting + boolean mHasUserAspectRatioSettingsButton; + + UserAspectRatioSettingsWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo, + @NonNull SyncTransactionQueue syncQueue, + @Nullable ShellTaskOrganizer.TaskListener taskListener, + @NonNull DisplayLayout displayLayout, @NonNull CompatUIHintsState compatUIHintsState, + @NonNull BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onButtonClicked, + @NonNull ShellExecutor shellExecutor) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + mShellExecutor = shellExecutor; + mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); + mCompatUIHintsState = compatUIHintsState; + mOnButtonClicked = onButtonClicked; + } + + @Override + protected int getZOrder() { + return TASK_CHILD_LAYER_COMPAT_UI + 1; + } + + @Override + protected @Nullable View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + return mHasUserAspectRatioSettingsButton; + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + mLayout.inject(this); + + updateVisibilityOfViews(); + + return mLayout; + } + + @VisibleForTesting + UserAspectRatioSettingsLayout inflateLayout() { + return (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate( + R.layout.user_aspect_ratio_settings_layout, null); + } + + @Override + public boolean updateCompatInfo(@NonNull TaskInfo taskInfo, + @NonNull ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + final boolean prevHasUserAspectRatioSettingsButton = mHasUserAspectRatioSettingsButton; + mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); + + if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { + return false; + } + + if (prevHasUserAspectRatioSettingsButton != mHasUserAspectRatioSettingsButton) { + updateVisibilityOfViews(); + } + return true; + } + + /** Called when the user aspect ratio settings button is clicked. */ + void onUserAspectRatioSettingsButtonClicked() { + mOnButtonClicked.accept(getLastTaskInfo(), getTaskListener()); + } + + /** Called when the user aspect ratio settings button is long clicked. */ + void onUserAspectRatioSettingsButtonLongClicked() { + if (mLayout == null) { + return; + } + mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true); + mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mShellExecutor.executeDelayed(this::hideUserAspectRatioButton, + HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + } + + @Override + @VisibleForTesting + public void updateSurfacePosition() { + if (mLayout == null) { + return; + } + // Position of the button in the container coordinate. + final Rect taskBounds = getTaskBounds(); + final Rect taskStableBounds = getTaskStableBounds(); + final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? taskStableBounds.left - taskBounds.left + : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth(); + final int positionY = taskStableBounds.bottom - taskBounds.top + - mLayout.getMeasuredHeight(); + updateSurfacePosition(positionX, positionY); + } + + @VisibleForTesting + void updateVisibilityOfViews() { + if (mHasUserAspectRatioSettingsButton) { + mShellExecutor.executeDelayed(this::showUserAspectRatioButton, + SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mShellExecutor.executeDelayed(this::hideUserAspectRatioButton, + HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + } else { + mShellExecutor.removeCallbacks(this::showUserAspectRatioButton); + mShellExecutor.execute(this::hideUserAspectRatioButton); + } + } + + private void showUserAspectRatioButton() { + if (mLayout == null) { + return; + } + mLayout.setUserAspectRatioButtonVisibility(true); + // Only show by default for the first time. + if (!mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint) { + mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true); + mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + } + } + + private void hideUserAspectRatioButton() { + if (mLayout == null || !isHideDelayReached(mNextButtonHideTimeMs)) { + return; + } + mLayout.setUserAspectRatioButtonVisibility(false); + } + + private boolean isHideDelayReached(long nextHideTime) { + return SystemClock.uptimeMillis() >= nextHideTime; + } + + private long updateHideTime(long hideDelay) { + return SystemClock.uptimeMillis() + hideDelay; + } + + private boolean getHasUserAspectRatioSettingsButton(@NonNull TaskInfo taskInfo) { + return taskInfo.topActivityEligibleForUserAspectRatioButton + && (taskInfo.topActivityBoundsLetterboxed + || taskInfo.isUserFullscreenOverrideEnabled); + } +} 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 0d55018ba580..19c60c2a9117 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -63,7 +63,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; -import android.os.SystemProperties; import android.view.Choreographer; import android.view.Display; import android.view.Surface; @@ -1735,17 +1734,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // animation. // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = mTaskInfo.topActivityInfo != null; - if (!hasTopActivityInfo) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "%s: TaskInfo.topActivityInfo is null", TAG); - } - if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", true) - && hasTopActivityInfo) { + if (hasTopActivityInfo) { animator.setAppIconContentOverlay( mContext, currentBounds, mTaskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } else { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "%s: TaskInfo.topActivityInfo is null", TAG); animator.setColorContentOverlay(mContext); } } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 2563d984b793..db7e2c0c529f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -49,7 +49,6 @@ import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; -import android.os.SystemProperties; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; @@ -902,17 +901,13 @@ public class PipTransition extends PipTransitionController { // animation. // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; - if (!hasTopActivityInfo) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "%s: TaskInfo.topActivityInfo is null", TAG); - } - if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", true) - && hasTopActivityInfo) { + if (hasTopActivityInfo) { animator.setAppIconContentOverlay( mContext, currentBounds, taskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } else { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "%s: TaskInfo.topActivityInfo is null", TAG); animator.setColorContentOverlay(mContext); } } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java index f9332e4bdb2e..2d3403599484 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java @@ -40,6 +40,7 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac "persist.wm.debug.enable_pip_keep_clear_algorithm_gravity", false); protected int mKeepClearAreasPadding; + private int mImeOffset; public PhonePipKeepClearAlgorithm(Context context) { reloadResources(context); @@ -48,6 +49,7 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac private void reloadResources(Context context) { final Resources res = context.getResources(); mKeepClearAreasPadding = res.getDimensionPixelSize(R.dimen.pip_keep_clear_areas_padding); + mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); } /** @@ -61,7 +63,7 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac Rect insets = new Rect(); pipBoundsAlgorithm.getInsetBounds(insets); if (pipBoundsState.isImeShowing()) { - insets.bottom -= pipBoundsState.getImeHeight(); + insets.bottom -= (pipBoundsState.getImeHeight() + mImeOffset); } // if PiP is stashed we only adjust the vertical position if it's outside of insets and // ignore all keep clear areas, since it's already on the side diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index a6501f05475f..efc69ebd395c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -58,6 +58,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,8 +68,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import dagger.Lazy; - /** * Tests for {@link CompatUIController}. * @@ -82,21 +82,36 @@ public class CompatUIControllerTest extends ShellTestCase { private CompatUIController mController; private ShellInit mShellInit; - private @Mock ShellController mMockShellController; - private @Mock DisplayController mMockDisplayController; - private @Mock DisplayInsetsController mMockDisplayInsetsController; - private @Mock DisplayLayout mMockDisplayLayout; - private @Mock DisplayImeController mMockImeController; - private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; - private @Mock SyncTransactionQueue mMockSyncQueue; - private @Mock ShellExecutor mMockExecutor; - private @Mock Lazy<Transitions> mMockTransitionsLazy; - private @Mock CompatUIWindowManager mMockCompatLayout; - private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout; - private @Mock RestartDialogWindowManager mMockRestartDialogLayout; - private @Mock DockStateReader mDockStateReader; - private @Mock CompatUIConfiguration mCompatUIConfiguration; - private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler; + @Mock + private ShellController mMockShellController; + @Mock + private DisplayController mMockDisplayController; + @Mock + private DisplayInsetsController mMockDisplayInsetsController; + @Mock + private DisplayLayout mMockDisplayLayout; + @Mock + private DisplayImeController mMockImeController; + @Mock + private ShellTaskOrganizer.TaskListener mMockTaskListener; + @Mock + private SyncTransactionQueue mMockSyncQueue; + @Mock + private ShellExecutor mMockExecutor; + @Mock + private Lazy<Transitions> mMockTransitionsLazy; + @Mock + private CompatUIWindowManager mMockCompatLayout; + @Mock + private LetterboxEduWindowManager mMockLetterboxEduLayout; + @Mock + private RestartDialogWindowManager mMockRestartDialogLayout; + @Mock + private DockStateReader mDockStateReader; + @Mock + private CompatUIConfiguration mCompatUIConfiguration; + @Mock + private CompatUIShellCommandHandler mCompatUIShellCommandHandler; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 5f294d53b662..3bce2b824e28 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -44,7 +44,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 78c3cbdaace6..4c837e635939 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -53,7 +53,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java index 973a99c269ea..a802f15a0a41 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -40,6 +40,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.BiConsumer; + /** * Tests for {@link ReachabilityEduWindowManager}. * @@ -57,6 +59,8 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; @Mock private DisplayLayout mDisplayLayout; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback; private TestShellExecutor mExecutor; private TaskInfo mTaskInfo; private ReachabilityEduWindowManager mWindowManager; @@ -104,6 +108,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) { return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue, - mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor); + mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor, + mOnDismissCallback); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java new file mode 100644 index 000000000000..1fee153877b5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; +import android.content.ComponentName; +import android.testing.AndroidTestingRunner; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.SurfaceControlViewHost; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.BiConsumer; + +/** + * Tests for {@link UserAspectRatioSettingsLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:UserAspectRatioSettingsLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock + private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> + mOnUserAspectRatioSettingsButtonClicked; + @Mock + private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock + private SurfaceControlViewHost mViewHost; + @Captor + private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor; + @Captor + private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor; + + private UserAspectRatioSettingsWindowManager mWindowManager; + private UserAspectRatioSettingsLayout mLayout; + private TaskInfo mTaskInfo; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, + mSyncTransactionQueue, mTaskListener, new DisplayLayout(), + new CompatUIController.CompatUIHintsState(), + mOnUserAspectRatioSettingsButtonClicked, new TestShellExecutor()); + + mLayout = (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate( + R.layout.user_aspect_ratio_settings_layout, null); + mLayout.inject(mWindowManager); + + spyOn(mWindowManager); + spyOn(mLayout); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + doReturn(mLayout).when(mWindowManager).inflateLayout(); + } + + @Test + public void testOnClickForUserAspectRatioSettingsButton() { + final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button); + button.performClick(); + + verify(mWindowManager).onUserAspectRatioSettingsButtonClicked(); + verify(mOnUserAspectRatioSettingsButtonClicked).accept( + mUserAspectRationTaskInfoCaptor.capture(), + mUserAspectRatioTaskListenerCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = + new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(), + mUserAspectRatioTaskListenerCaptor.getValue()); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); + } + + @Test + public void testOnLongClickForUserAspectRatioButton() { + doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked(); + + final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button); + button.performLongClick(); + + verify(mWindowManager).onUserAspectRatioSettingsButtonLongClicked(); + } + + @Test + public void testOnClickForUserAspectRatioSettingsHint() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + final LinearLayout sizeCompatHint = mLayout.findViewById( + R.id.user_aspect_ratio_settings_hint); + sizeCompatHint.performClick(); + + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false); + } + + private static TaskInfo createTaskInfo(boolean hasSizeCompat, + @CameraCompatControlState int cameraCompatControlState) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java new file mode 100644 index 000000000000..b48538ca99ca --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static android.view.WindowInsets.Type.navigationBars; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.content.ComponentName; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.util.Pair; +import android.view.DisplayInfo; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Tests for {@link UserAspectRatioSettingsWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:UserAspectRatioSettingsWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> + mOnUserAspectRatioSettingsButtonClicked; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private UserAspectRatioSettingsLayout mLayout; + @Mock private SurfaceControlViewHost mViewHost; + @Captor + private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor; + @Captor + private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor; + + private final Set<String> mPackageNameCache = new HashSet<>(); + + private UserAspectRatioSettingsWindowManager mWindowManager; + private TaskInfo mTaskInfo; + + private TestShellExecutor mExecutor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + mTaskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, + mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mOnUserAspectRatioSettingsButtonClicked, mExecutor); + spyOn(mWindowManager); + doReturn(mLayout).when(mWindowManager).inflateLayout(); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + } + + @Test + public void testCreateUserAspectRatioButton() { + // Doesn't create layout if show is false. + mWindowManager.mHasUserAspectRatioSettingsButton = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ false)); + + verify(mWindowManager, never()).inflateLayout(); + + // Doesn't create hint popup. + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + mExecutor.flushAll(); + verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true); + verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + + // Creates hint popup. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + mWindowManager.release(); + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = false; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + assertNotNull(mLayout); + mExecutor.flushAll(); + verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true); + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + assertTrue(mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint); + + // Returns false and doesn't create layout if mHasUserAspectRatioSettingsButton is false. + clearInvocations(mWindowManager); + mWindowManager.release(); + mWindowManager.mHasUserAspectRatioSettingsButton = false; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager, never()).inflateLayout(); + } + + @Test + public void testRelease() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + + mWindowManager.release(); + + verify(mViewHost).release(); + } + + @Test + public void testUpdateCompatInfo() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + + // No diff + clearInvocations(mWindowManager); + TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + true, /* topActivityBoundsLetterboxed */ true); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); + + verify(mWindowManager, never()).updateSurfacePosition(); + verify(mWindowManager, never()).release(); + verify(mWindowManager, never()).createLayout(anyBoolean()); + + + // Change task listener, recreate button. + clearInvocations(mWindowManager); + final ShellTaskOrganizer.TaskListener newTaskListener = mock( + ShellTaskOrganizer.TaskListener.class); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mWindowManager).release(); + verify(mWindowManager).createLayout(/* canShow= */ true); + + // Change has eligibleForUserAspectRatioButton to false, dispose the component + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + assertFalse( + mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + verify(mWindowManager).release(); + } + + @Test + public void testUpdateCompatInfoLayoutNotInflatedYet() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ false); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be + // inflated + clearInvocations(mWindowManager); + TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated. + clearInvocations(mWindowManager); + taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + true, /* topActivityBoundsLetterboxed */ true); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + } + + @Test + public void testUpdateDisplayLayout() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout1); + verify(mWindowManager).updateSurfacePosition(); + + // No update if the display bounds is the same. + clearInvocations(mWindowManager); + final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + mWindowManager.updateDisplayLayout(displayLayout2); + verify(mWindowManager, never()).updateSurfacePosition(); + } + + @Test + public void testUpdateDisplayLayoutInsets() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + + // Update if the insets change on the existing display layout + clearInvocations(mWindowManager); + InsetsState insetsState = new InsetsState(); + insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000)); + InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + insetsSource.setFrame(0, 1800, 1000, 2000); + insetsState.addSource(insetsSource); + displayLayout.setInsets(mContext.getResources(), insetsState); + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + } + + @Test + public void testUpdateVisibility() { + // Create button if it is not created. + mWindowManager.removeLayout(); + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.updateVisibility(/* canShow= */ true); + + verify(mWindowManager).createLayout(/* canShow= */ true); + + // Hide button. + clearInvocations(mWindowManager); + doReturn(View.VISIBLE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ false); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mLayout).setVisibility(View.GONE); + + // Show button. + doReturn(View.GONE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ true); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mLayout).setVisibility(View.VISIBLE); + } + + @Test + public void testAttachToParentSurface() { + final SurfaceControl.Builder b = new SurfaceControl.Builder(); + mWindowManager.attachToParentSurface(b); + + verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b); + } + + @Test + public void testOnUserAspectRatioButtonClicked() { + mWindowManager.onUserAspectRatioSettingsButtonClicked(); + + verify(mOnUserAspectRatioSettingsButtonClicked).accept( + mUserAspectRationTaskInfoCaptor.capture(), + mUserAspectRatioTaskListenerCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = + new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(), + mUserAspectRatioTaskListenerCaptor.getValue()); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); + } + + @Test + public void testOnUserAspectRatioButtonLongClicked_showHint() { + // Not create hint popup. + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + mWindowManager.createLayout(/* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + + mWindowManager.onUserAspectRatioSettingsButtonLongClicked(); + + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + } + + @Test + public void testWhenDockedStateHasChanged_needsToBeRecreated() { + ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); + newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK; + + Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); + } + + private static TaskInfo createTaskInfo(boolean eligibleForUserAspectRatioButton, + boolean topActivityBoundsLetterboxed) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityEligibleForUserAspectRatioButton = eligibleForUserAspectRatioButton; + taskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed; + taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; + taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); + return taskInfo; + } +} diff --git a/libs/androidfw/OWNERS b/libs/androidfw/OWNERS index 17f5164cf417..436f10737cb4 100644 --- a/libs/androidfw/OWNERS +++ b/libs/androidfw/OWNERS @@ -4,4 +4,4 @@ zyy@google.com patb@google.com per-file CursorWindow.cpp=omakoto@google.com -per-file LocaleDataTables.cpp=vichang@google.com,ngeoffray@google.com,nikitai@google.com +per-file LocaleDataTables.cpp=vichang@google.com,ngeoffray@google.com diff --git a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig index 0b74fa8ee361..d1bcb5746414 100644 --- a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig +++ b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig @@ -2,7 +2,7 @@ package: "com.android.settingslib.media.flags" flag { name: "use_media_router2_for_info_media_manager" - namespace: "placeholder_namespace" + namespace: "media_solutions" description: "Gates whether to use a MediaRouter2-based implementation of InfoMediaManager, instead of the legacy MediaRouter2Manager-based implementation." bug: "192657812" }
\ No newline at end of file diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java index ef062dfd3ec3..4b10b56f49fb 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -23,14 +23,16 @@ import static junit.framework.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.Cursor; import android.database.MatrixCursor; @@ -44,8 +46,8 @@ import android.provider.Settings; import android.telephony.TelephonyManager; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; +import android.util.ArrayMap; -import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; @@ -77,13 +79,14 @@ public class SettingsHelperTest { @Mock private Context mContext; @Mock private Resources mResources; - @Mock private ContentResolver mContentResolver; @Mock private AudioManager mAudioManager; @Mock private TelephonyManager mTelephonyManager; + @Mock private MockContentResolver mContentResolver; + private MockSettingsProvider mSettingsProvider; + @Before public void setUp() { - clearLongPressPowerValues(); MockitoAnnotations.initMocks(this); when(mContext.getSystemService(eq(Context.AUDIO_SERVICE))).thenReturn(mAudioManager); when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE))).thenReturn( @@ -91,14 +94,20 @@ public class SettingsHelperTest { when(mContext.getResources()).thenReturn(mResources); when(mContext.getApplicationContext()).thenReturn(mContext); when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); - when(mContext.getContentResolver()).thenReturn(getContentResolver()); mSettingsHelper = spy(new SettingsHelper(mContext)); + mContentResolver = spy(new MockContentResolver()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + mSettingsProvider = new MockSettingsProvider(mContext); + mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); } @After public void tearDown() { - clearLongPressPowerValues(); + Settings.Global.putString(mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, + null); + Settings.Global.putString(mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, + null); } @Test @@ -123,33 +132,30 @@ public class SettingsHelperTest { mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, SETTING_KEY, SETTING_VALUE, /* restoredFromSdkInt */ 0); - verifyZeroInteractions(mContentResolver); + // The only time of interaction happened during setUp() + verify(mContentResolver, times(1)) + .addProvider(Settings.AUTHORITY, mSettingsProvider); + + verifyNoMoreInteractions(mContentResolver); } @Test public void testRestoreValue_lppForAssistantEnabled_updatesValue() { - ContentResolver cr = - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); when(mResources.getBoolean( R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn( true); - mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY, + mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, Settings.Global.POWER_BUTTON_LONG_PRESS, "5", 0); - assertThat( - Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)) - .isEqualTo(5); - assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, - -1)).isEqualTo(2); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(5); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(2); } @Test public void testRestoreValue_lppForAssistantNotEnabled_updatesValueToDefaultConfig() { - ContentResolver cr = - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); when(mResources.getBoolean( R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn( true); @@ -161,21 +167,17 @@ public class SettingsHelperTest { R.integer.config_keyChordPowerVolumeUp)).thenReturn( 1); - mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY, + mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, Settings.Global.POWER_BUTTON_LONG_PRESS, "2", 0); - assertThat( - Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)) - .isEqualTo(1); - assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, - -1)).isEqualTo(1); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(1); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(1); } @Test public void testRestoreValue_lppForAssistantNotEnabledDefaultConfig_updatesValue() { - ContentResolver cr = - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); when(mResources.getBoolean( R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn( true); @@ -187,47 +189,39 @@ public class SettingsHelperTest { R.integer.config_keyChordPowerVolumeUp)).thenReturn( 1); - mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY, + mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, Settings.Global.POWER_BUTTON_LONG_PRESS, "2", 0); - assertThat( - Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)) - .isEqualTo(1); - assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, - -1)).isEqualTo(1); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(1); + assertThat(Settings.Global.getInt( + mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(1); } @Test public void testRestoreValue_lppForAssistantNotAvailable_doesNotRestore() { - ContentResolver cr = - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); - when(mResources.getBoolean( - R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn( - false); + when(mResources.getBoolean(R.bool.config_longPressOnPowerForAssistantSettingAvailable)) + .thenReturn(false); - mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY, - Settings.Global.POWER_BUTTON_LONG_PRESS, "5", 0); + mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, + Settings.Global.POWER_BUTTON_LONG_PRESS, "500", 0); - assertThat((Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, - -1))).isEqualTo(-1); + assertThat((Settings.Global.getInt( + mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))).isEqualTo(-1); } @Test public void testRestoreValue_lppForAssistantInvalid_doesNotRestore() { - ContentResolver cr = - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); when(mResources.getBoolean( R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn( false); - mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY, + mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY, Settings.Global.POWER_BUTTON_LONG_PRESS, "trees", 0); - assertThat((Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, - -1))).isEqualTo(-1); + assertThat((Settings.Global.getInt( + mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))).isEqualTo(-1); } @Test @@ -363,9 +357,6 @@ public class SettingsHelperTest { final String newRingtoneValueCanonicalized = "content://media/internal/audio/media/100?title=Song&canonical=1"; - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - ContentProvider mockMediaContentProvider = new MockContentProvider(mContext) { @Override @@ -386,25 +377,22 @@ public class SettingsHelperTest { } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + resetRingtoneSettingsToDefault(); + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(DEFAULT_RINGTONE_VALUE); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.RINGTONE, sourceRingtoneValue, 0); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(newRingtoneValueCanonicalized); } @@ -417,9 +405,6 @@ public class SettingsHelperTest { final String newRingtoneValueCanonicalized = "content://0@media/external/audio/media/100?title=Song&canonical=1"; - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); cursor.addRow(new Object[] {100L}); @@ -458,24 +443,21 @@ public class SettingsHelperTest { } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); + resetRingtoneSettingsToDefault(); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.RINGTONE, sourceRingtoneValue, 0); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(newRingtoneValueCanonicalized); } @@ -488,9 +470,6 @@ public class SettingsHelperTest { final String newRingtoneValueCanonicalized = "content://0@media/external/audio/media/200?title=notificationPing&canonicalize=1"; - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); cursor.addRow(new Object[] {200L}); @@ -529,17 +508,14 @@ public class SettingsHelperTest { } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); + resetRingtoneSettingsToDefault(); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.NOTIFICATION_SOUND, @@ -548,7 +524,7 @@ public class SettingsHelperTest { assertThat( Settings.System.getString( - mMockContentResolver, Settings.System.NOTIFICATION_SOUND)) + mContentResolver, Settings.System.NOTIFICATION_SOUND)) .isEqualTo(newRingtoneValueCanonicalized); } @@ -561,9 +537,6 @@ public class SettingsHelperTest { final String newRingtoneValueCanonicalized = "content://0@media/external/audio/media/300?title=alarmSound&canonical=1"; - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); cursor.addRow(new Object[] {300L}); @@ -600,26 +573,29 @@ public class SettingsHelperTest { assertThat(selectionArgs).isEqualTo(new String[] {"alarmSound"}); return cursor; } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, + Bundle opts) { + return null; + } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); + resetRingtoneSettingsToDefault(); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.ALARM_ALERT, sourceRingtoneValue, 0); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.ALARM_ALERT)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT)) .isEqualTo(newRingtoneValueCanonicalized); } @@ -628,9 +604,6 @@ public class SettingsHelperTest { final String sourceRingtoneValue = "content://0@media/external/audio/media/1?title=Song&canonical=1"; - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - // This is to mock the case that there are multiple results by querying title + // ringtone_type. MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); @@ -651,32 +624,26 @@ public class SettingsHelperTest { } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); + resetRingtoneSettingsToDefault(); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.RINGTONE, sourceRingtoneValue, 0); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(DEFAULT_RINGTONE_VALUE); } @Test public void testRestoreValue_customRingtone_restoreSilentValue() { - MockContentResolver mMockContentResolver = new MockContentResolver(); - when(mContext.getContentResolver()).thenReturn(mMockContentResolver); - ContentProvider mockMediaContentProvider = new MockContentProvider(mContext) { @Override @@ -691,37 +658,46 @@ public class SettingsHelperTest { } }; - ContentProvider mockSettingsContentProvider = - new MockSettingsProvider(mContext, getContentResolver()); - mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); - resetRingtoneSettingsToDefault(mMockContentResolver); + resetRingtoneSettingsToDefault(); mSettingsHelper.restoreValue( mContext, - mMockContentResolver, + mContentResolver, new ContentValues(), Uri.EMPTY, Settings.System.RINGTONE, "_silent", 0); - assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(null); } - public static class MockSettingsProvider extends MockContentProvider { - ContentResolver mBaseContentResolver; - - public MockSettingsProvider(Context context, ContentResolver baseContentResolver) { + private static class MockSettingsProvider extends MockContentProvider { + private final ArrayMap<String, String> mKeyValueStore = new ArrayMap<>(); + MockSettingsProvider(Context context) { super(context); - this.mBaseContentResolver = baseContentResolver; } @Override public Bundle call(String method, String request, Bundle args) { - return mBaseContentResolver.call(Settings.AUTHORITY, method, request, args); + if (method.startsWith("PUT_")) { + mKeyValueStore.put(request, args.getString("value")); + return null; + } else if (method.startsWith("GET_")) { + return Bundle.forPair("value", mKeyValueStore.getOrDefault(request, "")); + } + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + String name = values.getAsString("name"); + String value = values.getAsString("value"); + mKeyValueStore.put(name, value); + return null; } } @@ -752,15 +728,13 @@ public class SettingsHelperTest { } private int getAutoRotationSettingValue() { - return Settings.System.getInt( - getContentResolver(), + return Settings.System.getInt(mContentResolver, Settings.System.ACCELEROMETER_ROTATION, /* default= */ -1); } private void setAutoRotationSettingValue(int value) { - Settings.System.putInt( - getContentResolver(), + Settings.System.putInt(mContentResolver, Settings.System.ACCELEROMETER_ROTATION, value ); @@ -769,7 +743,7 @@ public class SettingsHelperTest { private void restoreAutoRotationSetting(int newValue) { mSettingsHelper.restoreValue( mContext, - getContentResolver(), + mContentResolver, new ContentValues(), /* destination= */ Settings.System.CONTENT_URI, /* name= */ Settings.System.ACCELEROMETER_ROTATION, @@ -777,31 +751,19 @@ public class SettingsHelperTest { /* restoredFromSdkInt= */ 0); } - private ContentResolver getContentResolver() { - return InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); - } - - private void clearLongPressPowerValues() { - ContentResolver cr = InstrumentationRegistry.getInstrumentation().getTargetContext() - .getContentResolver(); - Settings.Global.putString(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, null); - Settings.Global.putString(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, null); - } - - private void resetRingtoneSettingsToDefault(ContentResolver contentResolver) { + private void resetRingtoneSettingsToDefault() { Settings.System.putString( - contentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE); + mContentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE); Settings.System.putString( - contentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE); + mContentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE); Settings.System.putString( - contentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE); + mContentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE); - assertThat(Settings.System.getString(contentResolver, Settings.System.RINGTONE)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE)) .isEqualTo(DEFAULT_RINGTONE_VALUE); - assertThat(Settings.System.getString(contentResolver, Settings.System.NOTIFICATION_SOUND)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.NOTIFICATION_SOUND)) .isEqualTo(DEFAULT_NOTIFICATION_VALUE); - assertThat(Settings.System.getString(contentResolver, Settings.System.ALARM_ALERT)) + assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT)) .isEqualTo(DEFAULT_ALARM_VALUE); } } 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 889c026a0568..dd71dfa0b008 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 @@ -48,6 +48,7 @@ fun SceneScope.Notifications( Column( modifier = modifier + .element(key = Notifications.Elements.Notifications) .fillMaxWidth() .defaultMinSize(minHeight = 300.dp) .clip(RoundedCornerShape(32.dp)) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt index 38712b01ae44..291617f8edde 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt @@ -1,11 +1,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.scene.ui.composable.QuickSettings fun TransitionBuilder.goneToQuickSettingsTransition() { spec = tween(durationMillis = 500) - fade(QuickSettings.rootElementKey) + translate(QuickSettings.rootElementKey, Edge.Top, true) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 1d57c1a377e5..45df2b1bb20c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -1,11 +1,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.scene.ui.composable.Shade fun TransitionBuilder.goneToShadeTransition() { spec = tween(durationMillis = 500) - fade(Shade.rootElementKey) + translate(Shade.rootElementKey, Edge.Top, true) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt index 9a8a3e2048d1..e63bc4e458eb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt @@ -1,11 +1,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.scene.ui.composable.QuickSettings fun TransitionBuilder.lockscreenToQuickSettingsTransition() { spec = tween(durationMillis = 500) - fade(QuickSettings.rootElementKey) + translate(QuickSettings.rootElementKey, Edge.Top, true) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt index 6c7964b30989..21a10b1bc936 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt @@ -10,6 +10,5 @@ fun TransitionBuilder.shadeToQuickSettingsTransition() { spec = tween(durationMillis = 500) translate(Notifications.Elements.Notifications, Edge.Bottom) - fade(Notifications.Elements.Notifications) timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index e539c955a3c6..e7a53e5baaf4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -62,6 +62,7 @@ class DefaultClockController( private val burmeseLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese) private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale) + protected var onSecondaryDisplay: Boolean = false override val events: DefaultClockEvents override val config = ClockConfig() @@ -142,6 +143,11 @@ class DefaultClockController( view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) recomputePadding(targetRegion) } + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) { + this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay + recomputePadding(null) + } } open fun recomputePadding(targetRegion: Rect?) {} @@ -182,13 +188,19 @@ class DefaultClockController( override fun recomputePadding(targetRegion: Rect?) { // We center the view within the targetRegion instead of within the parent // view by computing the difference and adding that to the padding. - val parent = view.parent - val yDiff = - if (targetRegion != null && parent is View && parent.isLaidOut()) - targetRegion.centerY() - parent.height / 2f - else 0f val lp = view.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = (-0.5f * view.bottom + yDiff).toInt() + lp.topMargin = + if (onSecondaryDisplay) { + // On the secondary display we don't want any additional top/bottom margin. + 0 + } else { + val parent = view.parent + val yDiff = + if (targetRegion != null && parent is View && parent.isLaidOut()) + targetRegion.centerY() - parent.height / 2f + else 0f + (-0.5f * view.bottom + yDiff).toInt() + } view.setLayoutParams(lp) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index d962732ba884..527f80072222 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -177,6 +177,9 @@ interface ClockFaceEvents { * targetRegion is relative to the parent view. */ fun onTargetRegionChanged(targetRegion: Rect?) + + /** Called to notify the clock about its display. */ + fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) } /** Tick rates for clocks */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index cf7d2c57923c..3d9645a3d983 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -58,9 +58,26 @@ public interface VolumeDialogController { void userActivity(); void getState(); - boolean areCaptionsEnabled(); - void setCaptionsEnabled(boolean isEnabled); - + /** + * Get Captions enabled state + * + * @param checkForSwitchState set true when we'd like to switch captions enabled state after + * getting the latest captions state. + */ + void getCaptionsEnabledState(boolean checkForSwitchState); + + /** + * Set Captions enabled state + * + * @param enabled the captions enabled state we'd like to update. + */ + void setCaptionsEnabledState(boolean enabled); + + /** + * Get Captions component state + * + * @param fromTooltip if it's triggered from tooltip. + */ void getCaptionsComponentState(boolean fromTooltip); @ProvidesInterface(version = StreamState.VERSION) @@ -192,7 +209,22 @@ public interface VolumeDialogController { void onScreenOff(); void onShowSafetyWarning(int flags); void onAccessibilityModeChanged(Boolean showA11yStream); + + /** + * Callback function for captions component state changed event + * + * @param isComponentEnabled the lateset captions component state. + * @param fromTooltip if it's triggered from tooltip. + */ void onCaptionComponentStateChanged(Boolean isComponentEnabled, Boolean fromTooltip); + + /** + * Callback function for captions enabled state changed event + * + * @param isEnabled the lateset captions enabled state. + * @param checkBeforeSwitch intend to switch captions enabled state after the callback. + */ + void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); // requires version 2 void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml new file mode 100644 index 000000000000..593f507f3c88 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +** +** Copyright 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. +*/ +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/presentation" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <com.android.keyguard.KeyguardStatusView + android:id="@+id/clock" + android:layout_width="410dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:orientation="vertical"> + + <include + android:id="@+id/keyguard_clock_container" + layout="@layout/keyguard_clock_switch" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </com.android.keyguard.KeyguardStatusView> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 12f13e9a2138..3a15ae4f17ff 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -127,6 +127,8 @@ frame when animating QS <-> QQS transition android:gravity="center_vertical" android:paddingStart="@dimen/shade_header_system_icons_padding_start" android:paddingEnd="@dimen/shade_header_system_icons_padding_end" + android:paddingTop="@dimen/shade_header_system_icons_padding_top" + android:paddingBottom="@dimen/shade_header_system_icons_padding_bottom" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/privacy_container" app:layout_constraintTop_toTopOf="@id/clock"> diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml index 9af46c5b739c..37964158a4aa 100644 --- a/packages/SystemUI/res/layout/screen_share_dialog.xml +++ b/packages/SystemUI/res/layout/screen_share_dialog.xml @@ -64,8 +64,7 @@ android:layout_height="wrap_content" android:text="@string/screenrecord_permission_dialog_warning_entire_screen" style="@style/TextAppearance.Dialog.Body.Message" - android:gravity="start" - android:lineHeight="@dimen/screenrecord_warning_line_height"/> + android:gravity="start"/> <!-- Buttons --> <LinearLayout diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index d85e0122f2b4..915dcdb9755f 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -82,6 +82,8 @@ <!-- start padding is smaller to account for status icon margins coming from drawable itself --> <dimen name="shade_header_system_icons_padding_start">3dp</dimen> <dimen name="shade_header_system_icons_padding_end">4dp</dimen> + <dimen name="shade_header_system_icons_padding_top">2dp</dimen> + <dimen name="shade_header_system_icons_padding_bottom">2dp</dimen> <!-- Lockscreen shade transition values --> <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 5c42e45c7c6b..eb9d0b346418 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -923,4 +923,7 @@ "$packageName" part that will be replaced by the code with the package name of the target app. --> <string name="config_appStoreAppLinkTemplate" translatable="false"></string> + + <!-- Flag controlling whether visual query attention detection has been enabled. --> + <bool name="config_enableVisualQueryAttentionDetection">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8310b9548b66..2dc1b45af41f 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -496,9 +496,10 @@ <dimen name="large_screen_shade_header_min_height">@dimen/qs_header_row_min_height</dimen> <dimen name="large_screen_shade_header_left_padding">@dimen/qs_horizontal_margin</dimen> <dimen name="shade_header_system_icons_height">@dimen/large_screen_shade_header_min_height</dimen> - <dimen name="shade_header_system_icons_height_large_screen">32dp</dimen> <dimen name="shade_header_system_icons_padding_start">0dp</dimen> <dimen name="shade_header_system_icons_padding_end">0dp</dimen> + <dimen name="shade_header_system_icons_padding_top">0dp</dimen> + <dimen name="shade_header_system_icons_padding_bottom">0dp</dimen> <!-- The top margin of the panel that holds the list of notifications. On phones it's always 0dp but it's overridden in Car UI diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 261b08d4356f..0d45422f7369 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -39,7 +39,7 @@ <bool name="flag_battery_shield_icon">false</bool> <!-- Whether face auth will immediately stop when the display state is OFF --> - <bool name="flag_stop_face_auth_on_display_off">false</bool> + <bool name="flag_stop_face_auth_on_display_off">true</bool> <!-- Whether we want to stop pulsing while running the face scanning animation --> <bool name="flag_stop_pulsing_face_scanning_animation">true</bool> diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index 2ec6180513bf..fe61c46e341d 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -56,7 +56,7 @@ <Constraint android:id="@+id/shade_header_system_icons"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/shade_header_system_icons_height_large_screen" + android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/privacy_container" app:layout_constraintTop_toTopOf="parent" diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index c844db7b770d..77f6d03a03fb 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -28,8 +28,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public final class InteractionJankMonitorWrapper { - private static final String TAG = "JankMonitorWrapper"; - // Launcher journeys. public static final int CUJ_APP_LAUNCH_FROM_RECENTS = InteractionJankMonitor.CUJ_LAUNCHER_APP_LAUNCH_FROM_RECENTS; @@ -37,6 +35,8 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_LAUNCHER_APP_LAUNCH_FROM_ICON; public static final int CUJ_APP_CLOSE_TO_HOME = InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_HOME; + public static final int CUJ_APP_CLOSE_TO_HOME_FALLBACK = + InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK; public static final int CUJ_APP_CLOSE_TO_PIP = InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP; public static final int CUJ_QUICK_SWITCH = @@ -68,6 +68,7 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_LAUNCH_FROM_RECENTS, CUJ_APP_LAUNCH_FROM_ICON, CUJ_APP_CLOSE_TO_HOME, + CUJ_APP_CLOSE_TO_HOME_FALLBACK, CUJ_APP_CLOSE_TO_PIP, CUJ_QUICK_SWITCH, CUJ_APP_LAUNCH_FROM_WIDGET, diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt new file mode 100644 index 000000000000..899cad89a0be --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.app.Presentation +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.Display +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import com.android.keyguard.dagger.KeyguardStatusViewComponent +import com.android.systemui.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** [Presentation] shown in connected displays while on keyguard. */ +class ConnectedDisplayKeyguardPresentation +@AssistedInject +constructor( + @Assisted display: Display, + context: Context, + private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory, +) : + Presentation( + context, + display, + R.style.Theme_SystemUI_KeyguardPresentation, + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG + ) { + + private lateinit var keyguardStatusViewController: KeyguardStatusViewController + private lateinit var clock: KeyguardStatusView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView( + LayoutInflater.from(context) + .inflate(R.layout.keyguard_clock_presentation, /* root= */ null) + ) + val window = window ?: error("no window available.") + + // Logic to make the lock screen fullscreen + window.decorView.systemUiVisibility = + (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_STABLE) + window.attributes.fitInsetsTypes = 0 + window.isNavigationBarContrastEnforced = false + window.navigationBarColor = Color.TRANSPARENT + + clock = findViewById(R.id.clock) + keyguardStatusViewController = + keyguardStatusViewComponentFactory.build(clock).keyguardStatusViewController.apply { + setDisplayedOnSecondaryDisplay() + init() + } + } + + /** [ConnectedDisplayKeyguardPresentation] factory. */ + @AssistedFactory + interface Factory { + /** Creates a new [Presentation] for the given [display]. */ + fun create( + display: Display, + ): ConnectedDisplayKeyguardPresentation + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index b81e08183cdc..e3f9de11bf98 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -102,6 +102,12 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey super.onViewAttached(); mView.setKeyDownListener(mKeyDownListener); mEmergencyButtonController.setEmergencyButtonCallback(mEmergencyButtonCallback); + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (shouldLockout(deadline)) { + handleAttemptLockout(deadline); + } } @Override @@ -278,12 +284,6 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey @Override public void onResume(int reason) { mResumed = true; - // if the user is currently locked out, enforce it. - long deadline = mLockPatternUtils.getLockoutAttemptDeadline( - KeyguardUpdateMonitor.getCurrentUser()); - if (shouldLockout(deadline)) { - handleAttemptLockout(deadline); - } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 30b8ed0f1750..b5898870539f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -1,10 +1,5 @@ package com.android.keyguard; -import static android.view.View.ALPHA; -import static android.view.View.SCALE_X; -import static android.view.View.SCALE_Y; -import static android.view.View.TRANSLATION_Y; - import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; @@ -149,6 +144,13 @@ public class KeyguardClockSwitch extends RelativeLayout { updateStatusArea(/* animate= */false); } + /** Sets whether the large clock is being shown on a connected display. */ + public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) { + if (mClock != null) { + mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay); + } + } + /** * Enable or disable split shade specific positioning */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 3d48f3cc5359..dd39f1d6ed28 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -104,6 +104,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; + private boolean mShownOnSecondaryDisplay = false; private boolean mOnlyClock = false; private boolean mIsActiveDreamLockscreenHosted = false; private FeatureFlags mFeatureFlags; @@ -185,8 +186,18 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } /** + * When set, limits the information shown in an external display. + */ + public void setShownOnSecondaryDisplay(boolean shownOnSecondaryDisplay) { + mShownOnSecondaryDisplay = shownOnSecondaryDisplay; + } + + /** * Mostly used for alternate displays, limit the information shown + * + * @deprecated use {@link KeyguardClockSwitchController#setShownOnSecondaryDisplay} */ + @Deprecated public void setOnlyClock(boolean onlyClock) { mOnlyClock = onlyClock; } @@ -221,6 +232,15 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } } + private void hideSliceViewAndNotificationIconContainer() { + View ksv = mView.findViewById(R.id.keyguard_slice_view); + ksv.setVisibility(View.GONE); + + View nic = mView.findViewById( + R.id.left_aligned_notification_icon_container); + nic.setVisibility(View.GONE); + } + @Override protected void onViewAttached() { mClockRegistry.registerClockChangeListener(mClockChangedListener); @@ -234,13 +254,15 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardDateWeatherViewInvisibility = mView.getResources().getInteger(R.integer.keyguard_date_weather_view_invisibility); - if (mOnlyClock) { - View ksv = mView.findViewById(R.id.keyguard_slice_view); - ksv.setVisibility(View.GONE); + if (mShownOnSecondaryDisplay) { + mView.setLargeClockOnSecondaryDisplay(true); + displayClock(LARGE, /* animate= */ false); + hideSliceViewAndNotificationIconContainer(); + return; + } - View nic = mView.findViewById( - R.id.left_aligned_notification_icon_container); - nic.setVisibility(View.GONE); + if (mOnlyClock) { + hideSliceViewAndNotificationIconContainer(); return; } updateAodIcons(); @@ -293,6 +315,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS setClock(null); mSecureSettings.unregisterContentObserver(mDoubleLineClockObserver); + mSecureSettings.unregisterContentObserver(mShowWeatherObserver); mKeyguardUnlockAnimationController.removeKeyguardUnlockAnimationListener( mKeyguardUnlockAnimationListener); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index 9f21a31bd2c0..1c5a5758c2a4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -43,16 +43,19 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.statusbar.policy.KeyguardStateController; +import dagger.Lazy; + import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; @SysUISingleton public class KeyguardDisplayManager { @@ -64,6 +67,9 @@ public class KeyguardDisplayManager { private final DisplayTracker mDisplayTracker; private final Lazy<NavigationBarController> mNavigationBarControllerLazy; private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; + private final ConnectedDisplayKeyguardPresentation.Factory + mConnectedDisplayKeyguardPresentationFactory; + private final FeatureFlags mFeatureFlags; private final Context mContext; private boolean mShowing; @@ -105,7 +111,10 @@ public class KeyguardDisplayManager { @Main Executor mainExecutor, @UiBackground Executor uiBgExecutor, DeviceStateHelper deviceStateHelper, - KeyguardStateController keyguardStateController) { + KeyguardStateController keyguardStateController, + ConnectedDisplayKeyguardPresentation.Factory + connectedDisplayKeyguardPresentationFactory, + FeatureFlags featureFlags) { mContext = context; mNavigationBarControllerLazy = navigationBarControllerLazy; mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; @@ -115,6 +124,8 @@ public class KeyguardDisplayManager { mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor); mDeviceStateHelper = deviceStateHelper; mKeyguardStateController = keyguardStateController; + mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory; + mFeatureFlags = featureFlags; } private boolean isKeyguardShowable(Display display) { @@ -185,8 +196,12 @@ public class KeyguardDisplayManager { return false; } - KeyguardPresentation createPresentation(Display display) { - return new KeyguardPresentation(mContext, display, mKeyguardStatusViewComponentFactory); + Presentation createPresentation(Display display) { + if (mFeatureFlags.isEnabled(Flags.ENABLE_CLOCK_KEYGUARD_PRESENTATION)) { + return mConnectedDisplayKeyguardPresentationFactory.create(display); + } else { + return new KeyguardPresentation(mContext, display, mKeyguardStatusViewComponentFactory); + } } /** diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 20e465693108..42dbc487d774 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -30,13 +30,13 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; +import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; +import com.android.systemui.bouncer.ui.BouncerMessageView; +import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; -import com.android.systemui.bouncer.ui.BouncerMessageView; -import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.log.BouncerLogger; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.ViewController; @@ -95,6 +95,12 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> @CallSuper protected void onViewAttached() { updateMessageAreaVisibility(); + if (TextUtils.isEmpty(mMessageAreaController.getMessage()) + && getInitialMessageResId() != 0) { + mMessageAreaController.setMessage( + mView.getResources().getString(getInitialMessageResId()), + /* animate= */ false); + } } private void updateMessageAreaVisibility() { @@ -147,12 +153,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { - if (TextUtils.isEmpty(mMessageAreaController.getMessage()) - && getInitialMessageResId() != 0) { - mMessageAreaController.setMessage( - mView.getResources().getString(getInitialMessageResId()), - /* animate= */ false); - } mView.startAppearAnimation(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 49f788ce8f75..a30b4479fe95 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -238,6 +238,12 @@ public class KeyguardPatternViewController } mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (deadline != 0) { + handleAttemptLockout(deadline); + } } @Override @@ -268,12 +274,6 @@ public class KeyguardPatternViewController @Override public void onResume(int reason) { super.onResume(reason); - // if the user is currently locked out, enforce it. - long deadline = mLockPatternUtils.getLockoutAttemptDeadline( - KeyguardUpdateMonitor.getCurrentUser()); - if (deadline != 0) { - handleAttemptLockout(deadline); - } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index b3e08c0bc69f..574a0591bd51 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -79,6 +79,10 @@ public class KeyguardPinViewController mPasswordEntry.setUserActivityListener(this::onUserInput); mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); + if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { + mPasswordEntry.setUsePinShapes(true); + updateAutoConfirmationState(); + } } protected void onUserInput() { @@ -100,10 +104,6 @@ public class KeyguardPinViewController @Override public void startAppearAnimation() { - if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { - mPasswordEntry.setUsePinShapes(true); - updateAutoConfirmationState(); - } super.startAppearAnimation(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index d9a1dc66928c..04692c48a123 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -84,6 +84,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.domain.interactor.UserInteractor; @@ -146,8 +147,19 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private int mLastOrientation; private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid; + private int mCurrentUser = UserHandle.USER_NULL; private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = - () -> showPrimarySecurityScreen(false); + new UserSwitcherController.UserSwitchCallback() { + @Override + public void onUserSwitched() { + if (mCurrentUser == KeyguardUpdateMonitor.getCurrentUser()) { + return; + } + mCurrentUser = KeyguardUpdateMonitor.getCurrentUser(); + showPrimarySecurityScreen(false); + reinflateViewFlipper((l) -> {}); + } + }; @VisibleForTesting final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() { @@ -343,7 +355,6 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onThemeChanged() { reloadColors(); - reset(); } @Override @@ -401,6 +412,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final UserInteractor mUserInteractor; private final Provider<AuthenticationInteractor> mAuthenticationInteractor; private final Provider<JavaAdapter> mJavaAdapter; + private final DeviceProvisionedController mDeviceProvisionedController; @Nullable private Job mSceneTransitionCollectionJob; @Inject @@ -429,6 +441,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard BouncerMessageInteractor bouncerMessageInteractor, Provider<JavaAdapter> javaAdapter, UserInteractor userInteractor, + DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, KeyguardTransitionInteractor keyguardTransitionInteractor, Provider<AuthenticationInteractor> authenticationInteractor @@ -463,6 +476,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mAuthenticationInteractor = authenticationInteractor; mJavaAdapter = javaAdapter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mDeviceProvisionedController = deviceProvisionedController; } @Override @@ -847,9 +861,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard // Shortcut for SIM PIN/PUK to go to directly to user's security screen or home SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); boolean isLockscreenDisabled = mLockPatternUtils.isLockScreenDisabled( - KeyguardUpdateMonitor.getCurrentUser()); - if (securityMode == SecurityMode.None || isLockscreenDisabled) { - finish = isLockscreenDisabled; + KeyguardUpdateMonitor.getCurrentUser()) + || !mDeviceProvisionedController.isUserSetup(targetUserId); + + if (securityMode == SecurityMode.None && isLockscreenDisabled) { + finish = true; eventSubtype = BOUNCER_DISMISS_SIM; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM; } else { @@ -1164,7 +1180,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } private void reloadColors() { - reinflateViewFlipper(controller -> mView.reloadColors()); + mView.reloadColors(); } /** Handles density or font scale changes. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 757022dd153b..c314586e4a21 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -187,6 +187,11 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mConfigurationController.removeCallback(mConfigurationListener); } + /** Sets the StatusView as shown on an external display. */ + public void setDisplayedOnSecondaryDisplay() { + mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true); + } + /** * Called in notificationPanelViewController to avoid leak */ diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 2b83e6b05bbc..590056f2f8c2 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -23,6 +23,8 @@ import android.service.voice.VoiceInteractionSession; import android.util.Log; import com.android.internal.app.AssistUtils; +import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.internal.app.IVisualQueryRecognitionStatusListener; import com.android.internal.app.IVoiceInteractionSessionListener; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -39,10 +41,13 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.settings.SecureSettings; -import javax.inject.Inject; - import dagger.Lazy; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + /** * Class to manage everything related to assist in SystemUI. */ @@ -78,6 +83,18 @@ public class AssistManager { void hide(); } + /** + * An interface for a listener that receives notification that visual query attention has + * either been gained or lost. + */ + public interface VisualQueryAttentionListener { + /** Called when visual query attention has been gained. */ + void onAttentionGained(); + + /** Called when visual query attention has been lost. */ + void onAttentionLost(); + } + private static final String TAG = "AssistManager"; // Note that VERBOSE logging may leak PII (e.g. transcription contents). @@ -127,6 +144,23 @@ public class AssistManager { private final SecureSettings mSecureSettings; private final DeviceProvisionedController mDeviceProvisionedController; + + private final List<VisualQueryAttentionListener> mVisualQueryAttentionListeners = + new ArrayList<>(); + + private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener = + new IVisualQueryDetectionAttentionListener.Stub() { + @Override + public void onAttentionGained() { + mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionGained); + } + + @Override + public void onAttentionLost() { + mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionLost); + } + }; + private final CommandQueue mCommandQueue; protected final AssistUtils mAssistUtils; @@ -157,6 +191,7 @@ public class AssistManager { mSecureSettings = secureSettings; registerVoiceInteractionSessionListener(); + registerVisualQueryRecognitionStatusListener(); mUiController = defaultUiController; @@ -266,6 +301,24 @@ public class AssistManager { mAssistUtils.hideCurrentSession(); } + /** + * Add the given {@link VisualQueryAttentionListener} to the list of listeners awaiting + * notification of gaining/losing visual query attention. + */ + public void addVisualQueryAttentionListener(VisualQueryAttentionListener listener) { + if (!mVisualQueryAttentionListeners.contains(listener)) { + mVisualQueryAttentionListeners.add(listener); + } + } + + /** + * Remove the given {@link VisualQueryAttentionListener} from the list of listeners awaiting + * notification of gaining/losing visual query attention. + */ + public void removeVisualQueryAttentionListener(VisualQueryAttentionListener listener) { + mVisualQueryAttentionListeners.remove(listener); + } + private void startAssistInternal(Bundle args, @NonNull ComponentName assistComponent, boolean isService) { if (isService) { @@ -326,6 +379,27 @@ public class AssistManager { null, null); } + private void registerVisualQueryRecognitionStatusListener() { + if (!mContext.getResources() + .getBoolean(R.bool.config_enableVisualQueryAttentionDetection)) { + return; + } + + mAssistUtils.subscribeVisualQueryRecognitionStatus( + new IVisualQueryRecognitionStatusListener.Stub() { + @Override + public void onStartPerceiving() { + mAssistUtils.enableVisualQueryDetection( + mVisualQueryDetectionAttentionListener); + } + + @Override + public void onStopPerceiving() { + mAssistUtils.disableVisualQueryDetection(); + } + }); + } + public void launchVoiceAssistFromKeyguard() { mAssistUtils.launchVoiceAssistFromKeyguard(); } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt index 1817ea9024fc..64bf688b3c5d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt @@ -36,7 +36,6 @@ import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINL import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST -import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R.string.bouncer_face_not_recognized import com.android.systemui.R.string.keyguard_enter_password import com.android.systemui.R.string.keyguard_enter_pattern @@ -80,13 +79,14 @@ import com.android.systemui.R.string.kg_wrong_pin_try_again import com.android.systemui.bouncer.shared.model.BouncerMessageModel import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import javax.inject.Inject @SysUISingleton class BouncerMessageFactory @Inject constructor( - private val updateMonitor: KeyguardUpdateMonitor, + private val biometricSettingsRepository: BiometricSettingsRepository, private val securityModel: KeyguardSecurityModel, ) { @@ -99,7 +99,7 @@ constructor( getBouncerMessage( reason, securityModel.getSecurityMode(userId), - updateMonitor.isUnlockingWithFingerprintAllowed + biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed.value ) return pair?.let { BouncerMessageModel( diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt index 6fb0d4cc5e13..97c1bdb180a1 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt @@ -123,20 +123,11 @@ constructor( fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, ) : BouncerMessageRepository { - private val isFaceEnrolledAndEnabled = - and( - biometricSettingsRepository.isFaceAuthenticationEnabled, - biometricSettingsRepository.isFaceEnrolled - ) - - private val isFingerprintEnrolledAndEnabled = - and( - biometricSettingsRepository.isFingerprintEnabledByDevicePolicy, - biometricSettingsRepository.isFingerprintEnrolled - ) - private val isAnyBiometricsEnabledAndEnrolled = - or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled) + or( + biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, + biometricSettingsRepository.isFingerprintEnrolledAndEnabled, + ) private val wasRebootedForMainlineUpdate get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE @@ -335,8 +326,5 @@ constructor( } } -private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) = - flow.combine(anotherFlow) { a, b -> a && b } - private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) = flow.combine(anotherFlow) { a, b -> a || b } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt index 98ae54b1340e..9a7fec1daae0 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt @@ -83,9 +83,7 @@ constructor( fun canShowAlternateBouncerForFingerprint(): Boolean { return bouncerRepository.alternateBouncerUIAvailable.value && - biometricSettingsRepository.isFingerprintEnrolled.value && - biometricSettingsRepository.isStrongBiometricAllowed.value && - biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value && + biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed.value && !keyguardUpdateMonitor.isFingerprintLockedOut && !keyguardStateController.isUnlocked && !statusBarStateController.isDozing diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java index 566a74ae3e07..d58fab45093d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java @@ -27,7 +27,8 @@ import android.text.TextUtils; import com.android.systemui.R; class IntentCreator { - private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard"; + private static final String EXTRA_EDIT_SOURCE = "edit_source"; + private static final String EDIT_SOURCE_CLIPBOARD = "clipboard"; private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; static Intent getTextEditorIntent(Context context) { @@ -74,7 +75,7 @@ class IntentCreator { editIntent.setDataAndType(uri, "image/*"); editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - editIntent.putExtra(EXTRA_EDIT_SOURCE_CLIPBOARD, true); + editIntent.putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_CLIPBOARD); return editIntent; } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt index 8029ba844850..534832c0992d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt @@ -54,9 +54,10 @@ interface ControlActionCoordinator { /** * When a ToggleRange control is interacting with, a drag event is sent. * + * @param cvh [ControlViewHolder] for the control * @param isEdge did the drag event reach a control edge */ - fun drag(isEdge: Boolean) + fun drag(cvh: ControlViewHolder, isEdge: Boolean) /** * Send a request to update the value of a device using the [FloatAction]. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index a7e9efd8ab03..00d95c02c172 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -37,6 +37,8 @@ import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController @@ -57,6 +59,7 @@ class ControlActionCoordinatorImpl @Inject constructor( private val controlsMetricsLogger: ControlsMetricsLogger, private val vibrator: VibratorHelper, private val controlsSettingsRepository: ControlsSettingsRepository, + private val featureFlags: FeatureFlags, ) : ControlActionCoordinator { private var dialog: Dialog? = null private var pendingAction: Action? = null @@ -119,11 +122,17 @@ class ControlActionCoordinatorImpl @Inject constructor( ) } - override fun drag(isEdge: Boolean) { - if (isEdge) { - vibrate(Vibrations.rangeEdgeEffect) + override fun drag(cvh: ControlViewHolder, isEdge: Boolean) { + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + val constant = + if (isEdge) + HapticFeedbackConstants.SEGMENT_TICK + else + HapticFeedbackConstants.SEGMENT_FREQUENT_TICK + vibrator.performHapticFeedback(cvh.layout, constant) } else { - vibrate(Vibrations.rangeMiddleEffect) + val effect = if (isEdge) Vibrations.rangeEdgeEffect else Vibrations.rangeMiddleEffect + vibrate(effect) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index b2c95a63ad72..0d570d2dcc73 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -235,7 +235,7 @@ class ToggleRangeBehavior : Behavior { if (isDragging) { val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL if (clipLayer.level != newLevel) { - cvh.controlActionCoordinator.drag(isEdge) + cvh.controlActionCoordinator.drag(cvh, isEdge) clipLayer.level = newLevel } } else if (newLevel != clipLayer.level) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index d9665c5b5047..484be9ce1975 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -53,6 +53,7 @@ import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.statusbar.phone.LockscreenWallpaper import com.android.systemui.statusbar.phone.ScrimController +import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController @@ -331,4 +332,11 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(ScrimController::class) abstract fun bindScrimController(impl: ScrimController): CoreStartable + + @Binds + @IntoMap + @ClassKey(StatusBarHeadsUpChangeListener::class) + abstract fun bindStatusBarHeadsUpChangeListener( + impl: StatusBarHeadsUpChangeListener + ): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java index c889ac214cda..4dd97d557b30 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java @@ -16,10 +16,9 @@ package com.android.systemui.dreams.conditions; -import com.android.internal.app.AssistUtils; -import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.shared.condition.Condition; import javax.inject.Inject; @@ -30,12 +29,10 @@ import kotlinx.coroutines.CoroutineScope; * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention. */ public class AssistantAttentionCondition extends Condition { - private final DreamOverlayStateController mDreamOverlayStateController; - private final AssistUtils mAssistUtils; - private boolean mEnabled; + private final AssistManager mAssistManager; - private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener = - new IVisualQueryDetectionAttentionListener.Stub() { + private final VisualQueryAttentionListener mVisualQueryAttentionListener = + new VisualQueryAttentionListener() { @Override public void onAttentionGained() { updateCondition(true); @@ -47,59 +44,26 @@ public class AssistantAttentionCondition extends Condition { } }; - private final DreamOverlayStateController.Callback mCallback = - new DreamOverlayStateController.Callback() { - @Override - public void onStateChanged() { - if (mDreamOverlayStateController.isDreamOverlayStatusBarVisible()) { - enableVisualQueryDetection(); - } else { - disableVisualQueryDetection(); - } - } - }; - @Inject public AssistantAttentionCondition( @Application CoroutineScope scope, - DreamOverlayStateController dreamOverlayStateController, - AssistUtils assistUtils) { + AssistManager assistManager) { super(scope); - mDreamOverlayStateController = dreamOverlayStateController; - mAssistUtils = assistUtils; + mAssistManager = assistManager; } @Override protected void start() { - mDreamOverlayStateController.addCallback(mCallback); + mAssistManager.addVisualQueryAttentionListener(mVisualQueryAttentionListener); } @Override protected void stop() { - disableVisualQueryDetection(); - mDreamOverlayStateController.removeCallback(mCallback); + mAssistManager.removeVisualQueryAttentionListener(mVisualQueryAttentionListener); } @Override protected int getStartStrategy() { return START_EAGERLY; } - - private void enableVisualQueryDetection() { - if (mEnabled) { - return; - } - mEnabled = true; - mAssistUtils.enableVisualQueryDetection(mVisualQueryDetectionAttentionListener); - } - - private void disableVisualQueryDetection() { - if (!mEnabled) { - return; - } - mEnabled = false; - mAssistUtils.disableVisualQueryDetection(); - // Make sure the condition is set to false as well. - updateCondition(false); - } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 4a22a6732ce7..f3900ac1c3a8 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -115,7 +115,7 @@ object Flags { // TODO(b/292213543): Tracking Bug @JvmField val NOTIFICATION_GROUP_EXPANSION_CHANGE = - unreleasedFlag("notification_group_expansion_change", teamfood = false) + unreleasedFlag("notification_group_expansion_change", teamfood = true) // 200 - keyguard/lockscreen // ** Flag retired ** @@ -195,7 +195,7 @@ object Flags { // TODO(b/294110497): Tracking Bug @JvmField val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS = - unreleasedFlag("enable_wallet_contextual_loyalty_cards", teamfood = true) + releasedFlag("enable_wallet_contextual_loyalty_cards") // TODO(b/242908637): Tracking Bug @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag("wallpaper_fullscreen_preview") @@ -386,8 +386,7 @@ object Flags { @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag("new_bluetooth_repository") // TODO(b/292533677): Tracking Bug - val WIFI_TRACKER_LIB_FOR_WIFI_ICON = - unreleasedFlag("wifi_tracker_lib_for_wifi_icon", teamfood = true) + val WIFI_TRACKER_LIB_FOR_WIFI_ICON = releasedFlag("wifi_tracker_lib_for_wifi_icon") // TODO(b/293863612): Tracking Bug @JvmField val INCOMPATIBLE_CHARGING_BATTERY_ICON = @@ -545,12 +544,6 @@ object Flags { val ENABLE_PIP_SIZE_LARGE_SCREEN = sysPropBooleanFlag("persist.wm.debug.enable_pip_size_large_screen", default = true) - // TODO(b/265998256): Tracking bug - @Keep - @JvmField - val ENABLE_PIP_APP_ICON_OVERLAY = - sysPropBooleanFlag("persist.wm.debug.enable_pip_app_icon_overlay", default = true) - // TODO(b/293252410) : Tracking Bug @JvmField @@ -776,6 +769,10 @@ object Flags { @JvmField val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag("oneway_haptics_api_migration") + /** TODO(b/296223317): Enables the new keyguard presentation containing a clock. */ + @JvmField + val ENABLE_CLOCK_KEYGUARD_PRESENTATION = unreleasedFlag("enable_clock_keyguard_presentation") + /** Enable the Compose implementation of the PeopleSpaceActivity. */ @JvmField val COMPOSE_PEOPLE_SPACE = unreleasedFlag("compose_people_space") @@ -787,4 +784,8 @@ object Flags { /** Enable the share wifi button in Quick Settings internet dialog. */ @JvmField val SHARE_WIFI_QS_BUTTON = unreleasedFlag("share_wifi_qs_button") + + /** Enable haptic slider component in the brightness slider */ + @JvmField + val HAPTIC_BRIGHTNESS_SLIDER = unreleasedFlag("haptic_brightness_slider") } diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt new file mode 100644 index 000000000000..3f2f67dbba37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt @@ -0,0 +1,67 @@ +/* + * 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.keyevent.domain.interactor + +import android.view.KeyEvent +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor +import javax.inject.Inject + +/** + * Sends key events to the appropriate interactors and then acts upon key events that haven't + * already been handled but should be handled by SystemUI. + */ +@SysUISingleton +class KeyEventInteractor +@Inject +constructor( + private val backActionInteractor: BackActionInteractor, + private val keyguardKeyEventInteractor: KeyguardKeyEventInteractor, +) { + fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (keyguardKeyEventInteractor.dispatchKeyEvent(event)) { + return true + } + + when (event.keyCode) { + KeyEvent.KEYCODE_BACK -> { + if (event.handleAction()) { + backActionInteractor.onBackRequested() + } + return true + } + } + return false + } + + fun interceptMediaKey(event: KeyEvent): Boolean { + return keyguardKeyEventInteractor.interceptMediaKey(event) + } + + fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { + return keyguardKeyEventInteractor.dispatchKeyEventPreIme(event) + } + + companion object { + // Most actions shouldn't be handled on the down event and instead handled on subsequent + // key events like ACTION_UP. + fun KeyEvent.handleAction(): Boolean { + return action != KeyEvent.ACTION_DOWN + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index ff3e77c46f1c..682e841c3521 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -28,6 +28,9 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FacePropertyRepository +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -68,34 +71,32 @@ import kotlinx.coroutines.flow.transformLatest * upstream changes. */ interface BiometricSettingsRepository { - /** Whether any fingerprints are enrolled for the current user. */ - val isFingerprintEnrolled: StateFlow<Boolean> - - /** Whether face authentication is enrolled for the current user. */ - val isFaceEnrolled: Flow<Boolean> - /** - * Whether face authentication is enabled/disabled based on system settings like device policy, - * biometrics setting. + * If the current user can enter the device using fingerprint. This is true if user has enrolled + * fingerprints and fingerprint auth is not disabled through settings/device policy */ - val isFaceAuthenticationEnabled: Flow<Boolean> + val isFingerprintEnrolledAndEnabled: StateFlow<Boolean> /** - * Whether the current user is allowed to use a strong biometric for device entry based on - * Android Security policies. If false, the user may be able to use primary authentication for - * device entry. + * If the current user can enter the device using fingerprint, right now. + * + * This returns true if there are no strong auth flags that restrict the user from using + * fingerprint and [isFingerprintEnrolledAndEnabled] is true */ - val isStrongBiometricAllowed: StateFlow<Boolean> + val isFingerprintAuthCurrentlyAllowed: StateFlow<Boolean> /** - * Whether the current user is allowed to use a convenience biometric for device entry based on - * Android Security policies. If false, the user may be able to use strong biometric or primary - * authentication for device entry. + * If the current user can use face auth to enter the device. This is true when the user has + * face auth enrolled, and is enabled in settings/device policy. */ - val isNonStrongBiometricAllowed: StateFlow<Boolean> + val isFaceAuthEnrolledAndEnabled: Flow<Boolean> - /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */ - val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> + /** + * If the current user can use face auth to enter the device right now. This is true when + * [isFaceAuthEnrolledAndEnabled] is true and strong auth settings allow face auth to run and + * face auth is supported by the current device posture. + */ + val isFaceAuthCurrentlyAllowed: Flow<Boolean> /** * Whether face authentication is supported for the current device posture. Face auth can be @@ -130,6 +131,8 @@ constructor( @Background backgroundDispatcher: CoroutineDispatcher, biometricManager: BiometricManager?, devicePostureRepository: DevicePostureRepository, + facePropertyRepository: FacePropertyRepository, + fingerprintPropertyRepository: FingerprintPropertyRepository, dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { @@ -165,7 +168,9 @@ constructor( } override fun dump(pw: PrintWriter, args: Array<String?>) { - pw.println("isFingerprintEnrolled=${isFingerprintEnrolled.value}") + pw.println("isFingerprintEnrolledAndEnabled=${isFingerprintEnrolledAndEnabled.value}") + pw.println("isFingerprintAuthCurrentlyAllowed=${isFingerprintAuthCurrentlyAllowed.value}") + pw.println("isNonStrongBiometricAllowed=${isNonStrongBiometricAllowed.value}") pw.println("isStrongBiometricAllowed=${isStrongBiometricAllowed.value}") pw.println("isFingerprintEnabledByDevicePolicy=${isFingerprintEnabledByDevicePolicy.value}") } @@ -180,7 +185,7 @@ constructor( user = UserHandle.ALL ) - override val isFingerprintEnrolled: StateFlow<Boolean> = + private val isFingerprintEnrolled: Flow<Boolean> = selectedUserId .flatMapLatest { currentUserId -> conflatedCallbackFlow { @@ -211,7 +216,7 @@ constructor( authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id) ) - override val isFaceEnrolled: Flow<Boolean> = + private val isFaceEnrolled: Flow<Boolean> = selectedUserId.flatMapLatest { selectedUserId: Int -> conflatedCallbackFlow { val callback = @@ -245,14 +250,6 @@ constructor( isFaceEnabledByBiometricsManager.map { biometricsEnabledForUser[userInfo.id] ?: false } } - override val isFaceAuthenticationEnabled: Flow<Boolean> - get() = - combine(isFaceEnabledByBiometricsManagerForCurrentUser, isFaceEnabledByDevicePolicy) { - biometricsManagerSetting, - devicePolicySetting -> - biometricsManagerSetting && devicePolicySetting - } - private val isFaceEnabledByDevicePolicy: Flow<Boolean> = combine(selectedUserId, devicePolicyChangedForAllUsers) { userId, _ -> devicePolicyManager.isFaceDisabled(userId) @@ -263,6 +260,13 @@ constructor( .flowOn(backgroundDispatcher) .distinctUntilChanged() + private val isFaceAuthenticationEnabled: Flow<Boolean> = + combine(isFaceEnabledByBiometricsManagerForCurrentUser, isFaceEnabledByDevicePolicy) { + biometricsManagerSetting, + devicePolicySetting -> + biometricsManagerSetting && devicePolicySetting + } + private val isFaceEnabledByBiometricsManager: Flow<Pair<Int, Boolean>> = conflatedCallbackFlow { val callback = @@ -283,7 +287,7 @@ constructor( // being registered. .stateIn(scope, SharingStarted.Eagerly, Pair(0, false)) - override val isStrongBiometricAllowed: StateFlow<Boolean> = + private val isStrongBiometricAllowed: StateFlow<Boolean> = strongAuthTracker.isStrongBiometricAllowed.stateIn( scope, SharingStarted.Eagerly, @@ -293,7 +297,7 @@ constructor( ) ) - override val isNonStrongBiometricAllowed: StateFlow<Boolean> = + private val isNonStrongBiometricAllowed: StateFlow<Boolean> = strongAuthTracker.isNonStrongBiometricAllowed.stateIn( scope, SharingStarted.Eagerly, @@ -303,7 +307,19 @@ constructor( ) ) - override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = + private val isFingerprintBiometricAllowed: Flow<Boolean> = + fingerprintPropertyRepository.strength.flatMapLatest { + if (it == SensorStrength.STRONG) isStrongBiometricAllowed + else isNonStrongBiometricAllowed + } + + private val isFaceBiometricsAllowed: Flow<Boolean> = + facePropertyRepository.sensorInfo.flatMapLatest { + if (it?.strength == SensorStrength.STRONG) isStrongBiometricAllowed + else isNonStrongBiometricAllowed + } + + private val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = selectedUserId .flatMapLatest { userId -> devicePolicyChangedForAllUsers @@ -319,6 +335,25 @@ constructor( userRepository.getSelectedUserInfo().id ) ) + + override val isFingerprintEnrolledAndEnabled: StateFlow<Boolean> = + isFingerprintEnrolled + .and(isFingerprintEnabledByDevicePolicy) + .stateIn(scope, SharingStarted.Eagerly, false) + + override val isFingerprintAuthCurrentlyAllowed: StateFlow<Boolean> = + isFingerprintEnrolledAndEnabled + .and(isFingerprintBiometricAllowed) + .stateIn(scope, SharingStarted.Eagerly, false) + + override val isFaceAuthEnrolledAndEnabled: Flow<Boolean> + get() = isFaceAuthenticationEnabled.and(isFaceEnrolled) + + override val isFaceAuthCurrentlyAllowed: Flow<Boolean> + get() = + isFaceAuthEnrolledAndEnabled + .and(isFaceBiometricsAllowed) + .and(isFaceAuthSupportedInCurrentPosture) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index 689414711388..93eb103d74e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -26,8 +26,6 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.Dumpable import com.android.systemui.R -import com.android.systemui.biometrics.data.repository.FacePropertyRepository -import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -72,7 +70,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -162,7 +159,6 @@ constructor( @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val featureFlags: FeatureFlags, - facePropertyRepository: FacePropertyRepository, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null @@ -182,13 +178,6 @@ constructor( override val detectionStatus: Flow<FaceDetectionStatus> get() = _detectionStatus.filterNotNull() - private val isFaceBiometricsAllowed: Flow<Boolean> = - facePropertyRepository.sensorInfo.flatMapLatest { - if (it?.strength == SensorStrength.STRONG) - biometricSettingsRepository.isStrongBiometricAllowed - else biometricSettingsRepository.isNonStrongBiometricAllowed - } - private val _isLockedOut = MutableStateFlow(false) override val isLockedOut: StateFlow<Boolean> = _isLockedOut @@ -313,8 +302,10 @@ constructor( canFaceAuthOrDetectRun(faceDetectLog), logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog), logAndObserve( - isFaceBiometricsAllowed.isFalse().or(trustRepository.isCurrentUserTrusted), - "biometricIsNotAllowedOrCurrentUserIsTrusted", + biometricSettingsRepository.isFaceAuthCurrentlyAllowed + .isFalse() + .or(trustRepository.isCurrentUserTrusted), + "faceAuthIsNotCurrentlyAllowedOrCurrentUserIsTrusted", faceDetectLog ), // We don't want to run face detect if fingerprint can be used to unlock the device @@ -346,13 +337,8 @@ constructor( private fun canFaceAuthOrDetectRun(tableLogBuffer: TableLogBuffer): Flow<Boolean> { return listOf( logAndObserve( - biometricSettingsRepository.isFaceEnrolled, - "isFaceEnrolled", - tableLogBuffer - ), - logAndObserve( - biometricSettingsRepository.isFaceAuthenticationEnabled, - "isFaceAuthenticationEnabled", + biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, + "isFaceAuthEnrolledAndEnabled", tableLogBuffer ), logAndObserve(faceAuthPaused.isFalse(), "faceAuthIsNotPaused", tableLogBuffer), @@ -406,7 +392,11 @@ constructor( "currentUserIsNotTrusted", faceAuthLog ), - logAndObserve(isFaceBiometricsAllowed, "isFaceBiometricsAllowed", faceAuthLog), + logAndObserve( + biometricSettingsRepository.isFaceAuthCurrentlyAllowed, + "isFaceAuthCurrentlyAllowed", + faceAuthLog + ), logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog), ) .reduce(::and) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt new file mode 100644 index 000000000000..635961b0ea01 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.content.Context +import android.media.AudioManager +import android.view.KeyEvent +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor.Companion.handleAction +import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import javax.inject.Inject + +/** Handles key events arriving when the keyguard is showing or device is dozing. */ +@SysUISingleton +class KeyguardKeyEventInteractor +@Inject +constructor( + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, + private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + private val shadeController: ShadeController, + private val mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper, + private val backActionInteractor: BackActionInteractor, +) { + + fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (statusBarStateController.isDozing) { + when (event.keyCode) { + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_UP -> return dispatchVolumeKeyEvent(event) + } + } + + if (event.handleAction()) { + when (event.keyCode) { + KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent() + KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent() + } + } + return false + } + + /** + * While IME is active and a BACK event is detected, check with {@link + * StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event should be + * handled before routing to IME, in order to prevent the user from having to hit back twice to + * exit bouncer. + */ + fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { + when (event.keyCode) { + KeyEvent.KEYCODE_BACK -> + if ( + statusBarStateController.state == StatusBarState.KEYGUARD && + statusBarKeyguardViewManager.dispatchBackKeyEventPreIme() + ) { + return backActionInteractor.onBackRequested() + } + } + return false + } + + fun interceptMediaKey(event: KeyEvent): Boolean { + return statusBarStateController.state == StatusBarState.KEYGUARD && + statusBarKeyguardViewManager.interceptMediaKey(event) + } + + private fun dispatchMenuKeyEvent(): Boolean { + val shouldUnlockOnMenuPressed = + isDeviceInteractive() && + (statusBarStateController.state != StatusBarState.SHADE) && + statusBarKeyguardViewManager.shouldDismissOnMenuPressed() + if (shouldUnlockOnMenuPressed) { + shadeController.animateCollapseShadeForced() + return true + } + return false + } + + private fun dispatchSpaceEvent(): Boolean { + if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) { + shadeController.animateCollapseShadeForced() + return true + } + return false + } + + private fun dispatchVolumeKeyEvent(event: KeyEvent): Boolean { + mediaSessionLegacyHelperWrapper + .getHelper(context) + .sendVolumeKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE, true) + return true + } + + private fun isDeviceInteractive(): Boolean { + return keyguardInteractor.wakefulnessModel.value.isDeviceInteractive() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 23b80b05a785..ed4dd6a15c18 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -19,10 +19,10 @@ package com.android.systemui.keyguard.ui.binder import android.os.Trace import android.util.Log +import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.lifecycle.repeatWhenAttached import kotlinx.coroutines.launch @@ -31,19 +31,19 @@ class KeyguardBlueprintViewBinder { companion object { private const val TAG = "KeyguardBlueprintViewBinder" - fun bind(keyguardRootView: KeyguardRootView, viewModel: KeyguardBlueprintViewModel) { - keyguardRootView.repeatWhenAttached { + fun bind(constraintLayout: ConstraintLayout, viewModel: KeyguardBlueprintViewModel) { + constraintLayout.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.blueprint.collect { blueprint -> Trace.beginSection("KeyguardBlueprintController#applyBlueprint") Log.d(TAG, "applying blueprint: $blueprint") ConstraintSet().apply { - clone(keyguardRootView) + clone(constraintLayout) val emptyLayout = ConstraintSet.Layout() knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) } blueprint?.apply(this) - applyTo(keyguardRootView) + applyTo(constraintLayout) } Trace.endSection() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt new file mode 100644 index 000000000000..99243696a4e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.util + +import android.content.Context +import android.media.session.MediaSessionLegacyHelper +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** Injectable wrapper around `MediaSessionLegacyHelper` functions */ +@SysUISingleton +class MediaSessionLegacyHelperWrapper @Inject constructor() { + fun getHelper(context: Context): MediaSessionLegacyHelper { + return MediaSessionLegacyHelper.getHelper(context) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index e134f7c10b9b..ae0ab8423a99 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -25,6 +25,7 @@ import static android.app.StatusBarManager.WindowType; import static android.app.StatusBarManager.WindowVisibleState; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; @@ -1714,10 +1715,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) { final InsetsFrameProvider navBarProvider = - new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars()) - .setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] { - new InsetsFrameProvider.InsetsSizeOverride( - TYPE_INPUT_METHOD, null)}); + new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars()); + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + navBarProvider.setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] { + new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) + }); + } if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) { navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight)); } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 76d9b039e112..a7434c6ac797 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -21,16 +21,13 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.ObservableTransitionState -import com.android.systemui.scene.shared.model.RemoteUserInput import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -109,10 +106,6 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = repository.isVisible - private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null) - /** A flow of motion events originating from outside of the scene framework. */ - val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow() - /** * Returns the keys of all scenes in the container. * @@ -160,11 +153,6 @@ constructor( repository.setTransitionState(transitionState) } - /** Handles a remote user input. */ - fun onRemoteUserInput(input: RemoteUserInput) { - _remoteUserInput.value = input - } - /** * Notifies that the UI has transitioned sufficiently to the given scene. * diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt deleted file mode 100644 index 680de590a3fc..000000000000 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.systemui.scene.shared.model - -import android.view.MotionEvent - -/** A representation of user input that is used by the scene framework. */ -data class RemoteUserInput( - val x: Float, - val y: Float, - val action: RemoteUserInputAction, -) { - companion object { - fun translateMotionEvent(event: MotionEvent): RemoteUserInput { - return RemoteUserInput( - x = event.x, - y = event.y, - action = - when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN - MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE - MotionEvent.ACTION_UP -> RemoteUserInputAction.UP - MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL - else -> RemoteUserInputAction.UNKNOWN - } - ) - } - } -} - -enum class RemoteUserInputAction { - DOWN, - MOVE, - UP, - CANCEL, - UNKNOWN, -} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index 8601b3de2f7c..cdf50bab6b42 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -2,7 +2,6 @@ package com.android.systemui.scene.ui.view import android.content.Context import android.util.AttributeSet -import android.view.MotionEvent import android.view.View import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig @@ -39,14 +38,6 @@ class SceneWindowRootView( ) } - override fun onTouchEvent(event: MotionEvent?): Boolean { - return event?.let { - viewModel.onRemoteUserInput(event) - true - } - ?: false - } - override fun setVisibility(visibility: Int) { // Do nothing. We don't want external callers to invoke this. Instead, we drive our own // visibility from our view-binder. diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 3e9bbe464e2c..5c16fb54e3a0 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -16,11 +16,9 @@ package com.android.systemui.scene.ui.viewmodel -import android.view.MotionEvent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.ObservableTransitionState -import com.android.systemui.scene.shared.model.RemoteUserInput import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject @@ -34,9 +32,6 @@ class SceneContainerViewModel constructor( private val interactor: SceneInteractor, ) { - /** A flow of motion events originating from outside of the scene framework. */ - val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput - /** * Keys of all scenes in the container. * @@ -68,11 +63,6 @@ constructor( interactor.setTransitionState(transitionState) } - /** Handles a [MotionEvent] representing remote user input. */ - fun onRemoteUserInput(event: MotionEvent) { - interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event)) - } - companion object { private const val SCENE_TRANSITION_LOGGING_REASON = "user input" } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 05a0416f8f64..ab2a8d9c6e95 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -82,11 +82,15 @@ object ActionIntentCreator { return editIntent .setDataAndType(uri, "image/png") + .putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_SCREENSHOT) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } + + private const val EXTRA_EDIT_SOURCE = "edit_source" + private const val EDIT_SOURCE_SCREENSHOT = "screenshot" } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java index 0b4b7c691cfd..bdbc470a61f1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java @@ -43,6 +43,7 @@ import com.android.systemui.R; */ public class DraggableConstraintLayout extends ConstraintLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { + public static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe private static final float VELOCITY_DP_PER_MS = 1; private static final int MAXIMUM_DISMISS_DISTANCE_DP = 400; @@ -179,8 +180,13 @@ public class DraggableConstraintLayout extends ConstraintLayout Region r = new Region(); Rect rect = new Rect(); for (int i = 0; i < getChildCount(); i++) { - getChildAt(i).getGlobalVisibleRect(rect); - r.op(rect, Region.Op.UNION); + View child = getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + child.getGlobalVisibleRect(rect); + rect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + r.op(rect, Region.Op.UNION); + } } inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); inoutInfo.touchableRegion.set(r); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 3903bb2815ef..03e1e15c5210 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -68,7 +68,6 @@ import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ScrollCaptureResponse; -import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -123,7 +122,6 @@ public class ScreenshotView extends FrameLayout implements public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe private final Resources mResources; private final Interpolator mFastOutSlowIn; @@ -284,17 +282,22 @@ public class ScreenshotView extends FrameLayout implements Region swipeRegion = new Region(); final Rect tmpRect = new Rect(); + int swipePadding = (int) FloatingWindowUtil.dpToPx( + mDisplayMetrics, DraggableConstraintLayout.SWIPE_PADDING_DP * -1); mScreenshotPreview.getBoundsOnScreen(tmpRect); - tmpRect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + tmpRect.inset(swipePadding, swipePadding); swipeRegion.op(tmpRect, Region.Op.UNION); mActionsContainerBackground.getBoundsOnScreen(tmpRect); - tmpRect.inset((int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + tmpRect.inset(swipePadding, swipePadding); swipeRegion.op(tmpRect, Region.Op.UNION); mDismissButton.getBoundsOnScreen(tmpRect); swipeRegion.op(tmpRect, Region.Op.UNION); + View messageContainer = findViewById(R.id.screenshot_message_container); + if (messageContainer != null) { + messageContainer.getBoundsOnScreen(tmpRect); + swipeRegion.op(tmpRect, Region.Op.UNION); + } View messageDismiss = findViewById(R.id.message_dismiss_button); if (messageDismiss != null) { messageDismiss.getBoundsOnScreen(tmpRect); @@ -378,16 +381,6 @@ public class ScreenshotView extends FrameLayout implements mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip)); mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip)); - int swipePaddingPx = (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, SWIPE_PADDING_DP); - TouchDelegate previewDelegate = new TouchDelegate( - new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx), - mScreenshotPreview); - mScreenshotPreview.setTouchDelegate(previewDelegate); - TouchDelegate actionsDelegate = new TouchDelegate( - new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx), - mActionsContainerBackground); - mActionsContainerBackground.setTouchDelegate(actionsDelegate); - setFocusable(true); mActionsContainer.setScrollX(0); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 132cd6115bc7..df7d88fe9c90 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -163,6 +163,7 @@ import com.android.systemui.plugins.FalsingManager.FalsingTapListener; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; +import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shade.transition.ShadeTransitionController; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.QuickStepContract; @@ -355,6 +356,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final KeyguardRootView mKeyguardRootView; private final QuickSettingsController mQsController; + private final ShadeInteractor mShadeInteractor; private final TouchHandler mTouchHandler = new TouchHandler(); private long mDownTime; @@ -559,7 +561,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private float mHintDistance; private float mInitialOffsetOnTouch; private boolean mCollapsedAndHeadsUpOnDown; - private float mExpandedFraction = 0; private float mExpansionDragDownAmountPx = 0; private boolean mPanelClosedOnDown; private boolean mHasLayoutedSinceDown; @@ -709,10 +710,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump VibratorHelper vibratorHelper, LatencyTracker latencyTracker, PowerManager powerManager, - AccessibilityManager accessibilityManager, @DisplayId int displayId, + AccessibilityManager accessibilityManager, + @DisplayId int displayId, KeyguardUpdateMonitor keyguardUpdateMonitor, MetricsLogger metricsLogger, ShadeLogger shadeLogger, + ShadeInteractor shadeInteractor, ConfigurationController configurationController, Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder, StatusBarTouchableRegionManager statusBarTouchableRegionManager, @@ -785,6 +788,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mView = view; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mLockscreenGestureLogger = lockscreenGestureLogger; + mShadeInteractor = shadeInteractor; mShadeExpansionStateManager = shadeExpansionStateManager; mShadeLog = shadeLogger; mGutsManager = gutsManager; @@ -2244,7 +2248,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!isFalseTouch(x, y, interactionType)) { mShadeLog.logFlingExpands(vel, vectorVel, interactionType, this.mFlingAnimationUtils.getMinVelocityPxPerSecond(), - mExpandedFraction > 0.5f, mAllowExpandForSmallExpansion); + getExpandedFraction() > 0.5f, mAllowExpandForSmallExpansion); if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) { expands = shouldExpandWhenNotFlinging(); } else { @@ -2419,7 +2423,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void requestScrollerTopPaddingUpdate(boolean animate) { mNotificationStackScrollLayoutController.updateTopPadding( mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, - getKeyguardNotificationStaticPadding(), mExpandedFraction), animate); + getKeyguardNotificationStaticPadding(), getExpandedFraction()), animate); if (isKeyguardShowing() && mKeyguardBypassController.getBypassEnabled()) { // update the position of the header @@ -2501,10 +2505,10 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void onHeightUpdated(float expandedHeight) { if (expandedHeight <= 0) { mShadeLog.logExpansionChanged("onHeightUpdated: fully collapsed.", - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx); } else if (isFullyExpanded()) { mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.", - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx); } if (!mQsController.getExpanded() || mQsController.isExpandImmediate() || mIsExpandingOrCollapsing && mQsController.getExpandedWhenExpandingStarted()) { @@ -3198,6 +3202,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } + @Override + public void performHapticFeedback(int constant) { + mVibratorHelper.performHapticFeedback(mView, constant); + } + private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker { @Override public void addTrackingHeadsUpListener(Consumer<ExpandableNotificationRow> listener) { @@ -3425,7 +3434,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ipw.print("mHintDistance="); ipw.println(mHintDistance); ipw.print("mInitialOffsetOnTouch="); ipw.println(mInitialOffsetOnTouch); ipw.print("mCollapsedAndHeadsUpOnDown="); ipw.println(mCollapsedAndHeadsUpOnDown); - ipw.print("mExpandedFraction="); ipw.println(mExpandedFraction); + ipw.print("getExpandedFraction()="); ipw.println(getExpandedFraction()); ipw.print("mExpansionDragDownAmountPx="); ipw.println(mExpansionDragDownAmountPx); ipw.print("mPanelClosedOnDown="); ipw.println(mPanelClosedOnDown); ipw.print("mHasLayoutedSinceDown="); ipw.println(mHasLayoutedSinceDown); @@ -3761,7 +3770,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // don't fling while in keyguard to avoid jump in shade expand animation; // touch has been intercepted already so flinging here is redundant - if (mBarState == KEYGUARD && mExpandedFraction >= 1.0) { + if (mBarState == KEYGUARD && getExpandedFraction() >= 1.0) { mShadeLog.d("NPVC endMotionEvent - skipping fling on keyguard"); } else { fling(vel, expand, isFalseTouch(x, y, interactionType)); @@ -3857,6 +3866,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @VisibleForTesting void setExpandedHeight(float height) { debugLog("setExpandedHeight(%.1f)", height); + int maxPanelTransitionDistance = getMaxPanelTransitionDistance(); + if (maxPanelTransitionDistance == 0) { + setExpandedFracAndHeight(0, height); + } else { + setExpandedFracAndHeight(height / maxPanelTransitionDistance, height); + } + } + + private void setExpandedFracAndHeight(float frac, float height) { + mShadeInteractor.setExpansion(frac); setExpandedHeightInternal(height); } @@ -3912,11 +3931,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mHeightAnimator.end(); } } - mExpandedFraction = Math.min(1f, - maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); - mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction); + mQsController.setShadeExpansion(mExpandedHeight, getExpandedFraction()); mExpansionDragDownAmountPx = h; - mAmbientState.setExpansionFraction(mExpandedFraction); + mAmbientState.setExpansionFraction(getExpandedFraction()); onHeightUpdated(mExpandedHeight); updateExpansionAndVisibility(); }); @@ -3944,8 +3961,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Sets the expanded height relative to a number from 0 to 1. */ @VisibleForTesting void setExpandedFraction(float frac) { - final int maxDist = getMaxPanelTransitionDistance(); - setExpandedHeight(maxDist * frac); + setExpandedFracAndHeight(frac, getMaxPanelTransitionDistance() * frac); } float getExpandedHeight() { @@ -3953,7 +3969,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } float getExpandedFraction() { - return mExpandedFraction; + return mShadeInteractor.getExpansion().getValue(); } @Override @@ -3975,7 +3991,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public boolean isFullyCollapsed() { - return mExpandedFraction <= 0.0f; + return getExpandedFraction() <= 0.0f; } @Override @@ -4134,7 +4150,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump animator.getAnimatedFraction())); setOverExpansionInternal(expansion, false /* isFromGesture */); } - setExpandedHeightInternal((float) animation.getAnimatedValue()); + setExpandedHeight((float) animation.getAnimatedValue()); }); return animator; } @@ -4148,14 +4164,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void updateExpansionAndVisibility() { mShadeExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx); updateVisibility(); } @Override public boolean isExpanded() { - return mExpandedFraction > 0f + return getExpandedFraction() > 0f || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp() || mTracking @@ -4913,7 +4929,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mMotionAborted = false; mPanelClosedOnDown = isFullyCollapsed(); mShadeLog.logPanelClosedOnDown("intercept down touch", mPanelClosedOnDown, - mExpandedFraction); + getExpandedFraction()); mCollapsedAndHeadsUpOnDown = false; mHasLayoutedSinceDown = false; mUpdateFlingOnLayout = false; @@ -5132,7 +5148,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mMinExpandHeight = 0.0f; mPanelClosedOnDown = isFullyCollapsed(); mShadeLog.logPanelClosedOnDown("handle down touch", mPanelClosedOnDown, - mExpandedFraction); + getExpandedFraction()); mHasLayoutedSinceDown = false; mUpdateFlingOnLayout = false; mMotionAborted = false; @@ -5191,7 +5207,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (isFullyCollapsed()) { // If panel is fully collapsed, reset haptic effect before adding movement. mHasVibratedOnOpen = false; - mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction); + mShadeLog.logHasVibrated(mHasVibratedOnOpen, getExpandedFraction()); } addMovement(event); if (!isFullyCollapsed()) { @@ -5227,7 +5243,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // otherwise {@link NotificationStackScrollLayout} // wrongly enables stack height updates at the start of lockscreen swipe-up mAmbientState.setSwipingUp(h <= 0); - setExpandedHeightInternal(newHeight); + setExpandedHeight(newHeight); } break; diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index bea12de5ab49..656411874de5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -263,9 +263,11 @@ constructor( resources.getDimensionPixelSize( R.dimen.shade_header_system_icons_padding_start ), - systemIcons.paddingTop, + resources.getDimensionPixelSize(R.dimen.shade_header_system_icons_padding_top), resources.getDimensionPixelSize(R.dimen.shade_header_system_icons_padding_end), - systemIcons.paddingBottom + resources.getDimensionPixelSize( + R.dimen.shade_header_system_icons_padding_bottom + ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index d5b5c87ec781..182a676c9841 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -248,6 +248,16 @@ interface ShadeViewController { /** Starts tracking a shade expansion gesture that originated from the status bar. */ fun startTrackingExpansionFromStatusBar() + /** + * Performs haptic feedback from a view with a haptic feedback constant. + * + * The implementation of this method should use the [android.view.View.performHapticFeedback] + * method with the provided constant. + * + * @param[constant] One of [android.view.HapticFeedbackConstants] + */ + fun performHapticFeedback(constant: Int) + // ******* End Keyguard Section ********* /** Returns the ShadeHeadsUpTracker. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 287ac528385f..09b74b213ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -86,6 +86,8 @@ class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController { return false } override fun startTrackingExpansionFromStatusBar() {} + override fun performHapticFeedback(constant: Int) {} + override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl() override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index ebb9935ca813..76dca4ca5b03 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -34,13 +34,34 @@ interface ShadeRepository { /** ShadeModel information regarding shade expansion events */ val shadeModel: Flow<ShadeModel> - /** Amount qs has expanded. Quick Settings can be expanded without the full shade expansion. */ + /** + * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully + * expanded and `0` is fully collapsed. Quick Settings can be expanded without a fully expanded + * shade. + */ val qsExpansion: StateFlow<Float> + /** + * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully + * expanded and `0` is fully collapsed. + */ + val expansion: StateFlow<Float> + /** Amount shade has expanded with regard to the UDFPS location */ val udfpsTransitionToFullShadeProgress: StateFlow<Float> + /** + * Set shade expansion to a value from `0` to `1` representing the amount the shade has expanded + * where `1` is fully expanded and `0` is fully collapsed. + */ + fun setExpansion(expansion: Float) + + /** + * Set quick settings expansion to a value from `0` to `1` representing the amount quick + * settings has expanded where `1` is fully expanded and `0` is fully collapsed. + */ fun setQsExpansion(qsExpansion: Float) + fun setUdfpsTransitionToFullShadeProgress(progress: Float) } @@ -78,9 +99,17 @@ constructor(shadeExpansionStateManager: ShadeExpansionStateManager) : ShadeRepos private val _qsExpansion = MutableStateFlow(0f) override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow() + private val _expansion = MutableStateFlow(0f) + override val expansion: StateFlow<Float> = _expansion.asStateFlow() + private var _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f) override val udfpsTransitionToFullShadeProgress: StateFlow<Float> = _udfpsTransitionToFullShadeProgress.asStateFlow() + + override fun setExpansion(expansion: Float) { + _expansion.value = expansion + } + override fun setQsExpansion(qsExpansion: Float) { _qsExpansion.value = qsExpansion } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 6fde84a35fb1..efe3eb4db3ee 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.shade.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController @@ -38,18 +39,29 @@ class ShadeInteractor @Inject constructor( @Application scope: CoroutineScope, + private val shadeRepository: ShadeRepository, disableFlagsRepository: DisableFlagsRepository, keyguardRepository: KeyguardRepository, userSetupRepository: UserSetupRepository, deviceProvisionedController: DeviceProvisionedController, userInteractor: UserInteractor, ) { + /** + * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully + * expanded and `0` is fully collapsed. + */ + val expansion = shadeRepository.expansion + /** Emits true if the shade is currently allowed and false otherwise. */ val isShadeEnabled: StateFlow<Boolean> = disableFlagsRepository.disableFlags .map { it.isShadeEnabled() } .stateIn(scope, SharingStarted.Eagerly, initialValue = false) + fun setExpansion(expansion: Float) { + shadeRepository.setExpansion(expansion) + } + /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */ val isExpandToQsEnabled: Flow<Boolean> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 5c2f9a8d28ec..62a0d138fd05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -39,10 +39,7 @@ class StackCoordinator @Inject internal constructor( override fun attach(pipeline: NotifPipeline) { pipeline.addOnAfterRenderListListener(::onAfterRenderList) - // TODO(b/282865576): This has an issue where it makes changes to some groups without - // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the - // group expansion bug. - // groupExpansionManagerImpl.attach(pipeline) + groupExpansionManagerImpl.attach(pipeline) } fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index 46af03a438f5..5d33804ab6a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -67,18 +67,29 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl * Cleanup entries from mExpandedGroups that no longer exist in the pipeline. */ private final OnBeforeRenderListListener mNotifTracker = (entries) -> { + if (mExpandedGroups.isEmpty()) { + return; // nothing to do + } + final Set<NotificationEntry> renderingSummaries = new HashSet<>(); for (ListEntry entry : entries) { if (entry instanceof GroupEntry) { renderingSummaries.add(entry.getRepresentativeEntry()); } } - mExpandedGroups.removeIf(expandedGroup -> !renderingSummaries.contains(expandedGroup)); + + // Create a copy of mExpandedGroups so we can modify it in a thread-safe way. + final var currentExpandedGroups = new HashSet<>(mExpandedGroups); + for (NotificationEntry entry : currentExpandedGroups) { + setExpanded(entry, renderingSummaries.contains(entry)); + } }; public void attach(NotifPipeline pipeline) { - mDumpManager.registerDumpable(this); - pipeline.addOnBeforeRenderListListener(mNotifTracker); + if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) { + mDumpManager.registerDumpable(this); + pipeline.addOnBeforeRenderListListener(mNotifTracker); + } } @Override @@ -94,11 +105,24 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); + setExpanded(groupSummary, expanded); + } + + /** + * Add or remove {@code entry} to/from {@code mExpandedGroups} and notify listeners if + * something changed. This assumes that {@code entry} is a group summary. + * <p> + * TODO(b/293434635): Currently, in spite of its docs, + * {@code mGroupMembershipManager.getGroupSummary(entry)} returns null if {@code entry} is + * already a summary. Instead of needing this helper method to bypass that, we probably want to + * move this code back to {@code setGroupExpanded} and use that everywhere. + */ + private void setExpanded(NotificationEntry entry, boolean expanded) { boolean changed; if (expanded) { - changed = mExpandedGroups.add(groupSummary); + changed = mExpandedGroups.add(entry); } else { - changed = mExpandedGroups.remove(groupSummary); + changed = mExpandedGroups.remove(entry); } // Only notify listeners if something changed. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 6431ef958239..2bc7b996ffb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; @@ -33,12 +34,15 @@ import android.os.VibrationEffect; import android.os.Vibrator; import android.util.Log; import android.util.Slog; +import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.LetterboxDetails; @@ -49,6 +53,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.camera.CameraIntents; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; @@ -107,6 +112,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final Lazy<CameraLauncher> mCameraLauncherLazy; private final QuickSettingsController mQsController; private final QSHost mQSHost; + private final FeatureFlags mFeatureFlags; private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); @@ -144,7 +150,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba Lazy<CameraLauncher> cameraLauncherLazy, UserTracker userTracker, QSHost qsHost, - ActivityStarter activityStarter) { + ActivityStarter activityStarter, + FeatureFlags featureFlags) { mCentralSurfaces = centralSurfaces; mQsController = quickSettingsController; mContext = context; @@ -171,6 +178,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mCameraLauncherLazy = cameraLauncherLazy; mUserTracker = userTracker; mQSHost = qsHost; + mFeatureFlags = featureFlags; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( @@ -314,7 +322,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN); if (mShadeViewController.isFullyCollapsed()) { if (mVibrateOnOpening) { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + vibrateOnNavigationKeyDown(); } mShadeViewController.expand(true /* animate */); mNotificationStackScrollLayoutController.setWillExpand(true); @@ -587,4 +595,15 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } return VibrationEffect.createWaveform(timings, /* repeat= */ -1); } + + @VisibleForTesting + void vibrateOnNavigationKeyDown() { + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + mShadeViewController.performHapticFeedback( + HapticFeedbackConstants.GESTURE_START + ); + } else { + mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 127569d179fe..8ffd43a6eb89 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1612,8 +1612,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mBackActionInteractor.setup(mQsController, mShadeSurface); mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter(); - mHeadsUpManager.addListener(mCentralSurfacesComponent.getStatusBarHeadsUpChangeListener()); - // Listen for demo mode changes mDemoModeController.addCallback(mDemoModeCallback); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 931aedd90542..4de669c0b34a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -28,8 +28,7 @@ import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.RemoteUserInput +import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController @@ -56,7 +55,7 @@ class PhoneStatusBarViewController private constructor( private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, - private val sceneInteractor: Provider<SceneInteractor>, + private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, private val userChipViewModel: StatusBarUserChipViewModel, @@ -80,7 +79,8 @@ class PhoneStatusBarViewController private constructor( statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)) if (moveFromCenterAnimationController == null) return - val statusBarLeftSide: View = mView.requireViewById(R.id.status_bar_start_side_except_heads_up) + val statusBarLeftSide: View = + mView.requireViewById(R.id.status_bar_start_side_except_heads_up) val systemIconArea: ViewGroup = mView.requireViewById(R.id.status_bar_end_side_content) val viewsToAnimate = arrayOf( @@ -179,11 +179,8 @@ class PhoneStatusBarViewController private constructor( // If scene framework is enabled, route the touch to it and // ignore the rest of the gesture. if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { - sceneInteractor.get() - .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event)) - // TODO(b/291965119): remove once view is expanded to cover the status bar - sceneInteractor.get().setVisible(true, "swipe down from status bar") - return false + windowRootView.get().dispatchTouchEvent(event) + return true } if (event.action == MotionEvent.ACTION_DOWN) { @@ -247,7 +244,7 @@ class PhoneStatusBarViewController private constructor( private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, - private val sceneInteractor: Provider<SceneInteractor>, + private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, private val configurationController: ConfigurationController, @@ -269,7 +266,7 @@ class PhoneStatusBarViewController private constructor( centralSurfaces, shadeController, shadeViewController, - sceneInteractor, + windowRootView, shadeLogger, statusBarMoveFromCenterAnimationController, userChipViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java index 9a295e63fb9e..4b39854e43ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java @@ -16,23 +16,24 @@ package com.android.systemui.statusbar.phone; +import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.window.StatusBarWindowController; import javax.inject.Inject; /** - * Ties the {@link CentralSurfaces} to {@link com.android.systemui.statusbar.policy.HeadsUpManager}. + * Ties the status bar to {@link com.android.systemui.statusbar.policy.HeadsUpManager}. */ -@CentralSurfacesComponent.CentralSurfacesScope -public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener { +@SysUISingleton +public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener, CoreStartable { private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarWindowController mStatusBarWindowController; private final ShadeViewController mShadeViewController; @@ -63,6 +64,11 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener } @Override + public void start() { + mHeadsUpManager.addListener(this); + } + + @Override public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { if (inPinnedMode) { mNotificationShadeWindowController.setHeadsUpShowing(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java index 3a3663d4d6c4..bbbe16f54734 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java @@ -25,7 +25,6 @@ import com.android.systemui.shade.ShadeHeaderController; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks; import com.android.systemui.statusbar.phone.CentralSurfacesImpl; -import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarterModule; import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment; @@ -72,11 +71,6 @@ public interface CentralSurfacesComponent { WindowRootView getWindowRootView(); /** - * Creates a StatusBarHeadsUpChangeListener. - */ - StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener(); - - /** * Creates a CentralSurfacesCommandQueueCallbacks. */ CentralSurfacesCommandQueueCallbacks getCentralSurfacesCommandQueueCallbacks(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index fe2481595ff4..275cfc58d581 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -221,36 +221,15 @@ constructor( override val activityInVisible: Flow<Boolean> = activity .map { it?.hasActivityIn ?: false } - .distinctUntilChanged() - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnPrefix = "", - columnName = "activityInVisible", - initialValue = false, - ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val activityOutVisible: Flow<Boolean> = activity .map { it?.hasActivityOut ?: false } - .distinctUntilChanged() - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnPrefix = "", - columnName = "activityOutVisible", - initialValue = false, - ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val activityContainerVisible: Flow<Boolean> = activity .map { it != null && (it.hasActivityIn || it.hasActivityOut) } - .distinctUntilChanged() - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnPrefix = "", - columnName = "activityContainerVisible", - initialValue = false, - ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt index 4a31b8687069..eaae0f0c4f75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt @@ -33,6 +33,7 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.Dependency import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.ShadeLogger import com.android.systemui.util.ViewController import com.android.systemui.util.time.SystemClock import java.text.FieldPosition @@ -83,6 +84,7 @@ class VariableDateViewController( private val systemClock: SystemClock, private val broadcastDispatcher: BroadcastDispatcher, private val shadeExpansionStateManager: ShadeExpansionStateManager, + private val shadeLogger: ShadeLogger, private val timeTickHandler: Handler, view: VariableDateView ) : ViewController<VariableDateView>(view) { @@ -111,24 +113,29 @@ class VariableDateViewController( private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if ( + Intent.ACTION_LOCALE_CHANGED == action || + Intent.ACTION_TIMEZONE_CHANGED == action + ) { + // need to get a fresh date format + dateFormat = null + shadeLogger.d("VariableDateViewController received intent to refresh date format") + } + + val handler = mView.handler + // If the handler is null, it means we received a broadcast while the view has not // finished being attached or in the process of being detached. // In that case, do not post anything. - val handler = mView.handler ?: return - val action = intent.action - if ( + if (handler == null) { + shadeLogger.d("VariableDateViewController received intent but handler was null") + } else if ( Intent.ACTION_TIME_TICK == action || Intent.ACTION_TIME_CHANGED == action || Intent.ACTION_TIMEZONE_CHANGED == action || Intent.ACTION_LOCALE_CHANGED == action ) { - if ( - Intent.ACTION_LOCALE_CHANGED == action || - Intent.ACTION_TIMEZONE_CHANGED == action - ) { - // need to get a fresh date format - handler.post { dateFormat = null } - } handler.post(::updateClock) } } @@ -231,6 +238,7 @@ class VariableDateViewController( private val systemClock: SystemClock, private val broadcastDispatcher: BroadcastDispatcher, private val shadeExpansionStateManager: ShadeExpansionStateManager, + private val shadeLogger: ShadeLogger, @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler ) { fun create(view: VariableDateView): VariableDateViewController { @@ -238,6 +246,7 @@ class VariableDateViewController( systemClock, broadcastDispatcher, shadeExpansionStateManager, + shadeLogger, handler, view ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 53f4837cefcb..9cc3cdbf5c34 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -67,7 +67,6 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.VolumeDialogController; @@ -84,6 +83,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -134,7 +134,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final Receiver mReceiver = new Receiver(); private final RingerModeObservers mRingerModeObservers; private final MediaSessions mMediaSessions; - private CaptioningManager mCaptioningManager; + private final AtomicReference<CaptioningManager> mCaptioningManager = new AtomicReference<>(); private final KeyguardManager mKeyguardManager; private final ActivityManager mActivityManager; private final UserTracker mUserTracker; @@ -158,16 +158,16 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final WakefulnessLifecycle.Observer mWakefullnessLifecycleObserver = new WakefulnessLifecycle.Observer() { - @Override - public void onStartedWakingUp() { - mDeviceInteractive = true; - } + @Override + public void onStartedWakingUp() { + mDeviceInteractive = true; + } - @Override - public void onFinishedGoingToSleep() { - mDeviceInteractive = false; - } - }; + @Override + public void onFinishedGoingToSleep() { + mDeviceInteractive = false; + } + }; @Inject public VolumeDialogControllerImpl( @@ -185,8 +185,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, - DumpManager dumpManager, - @Main Handler mainHandler + DumpManager dumpManager ) { mContext = context.getApplicationContext(); mPackageManager = packageManager; @@ -215,7 +214,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mKeyguardManager = keyguardManager; mActivityManager = activityManager; mUserTracker = userTracker; - mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mainHandler)); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mWorker)); createCaptioningManagerServiceByUserContext(mUserTracker.getUserContext()); dumpManager.registerDumpable("VolumeDialogControllerImpl", this); @@ -223,8 +222,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa boolean accessibilityVolumeStreamActive = accessibilityManager .isAccessibilityVolumeStreamActive(); mVolumeController.setA11yMode(accessibilityVolumeStreamActive ? - VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME : - VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME); + VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME : + VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME); mWakefulnessLifecycle.addObserver(mWakefullnessLifecycleObserver); } @@ -337,15 +336,15 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa }; private void createCaptioningManagerServiceByUserContext(@NonNull Context userContext) { - mCaptioningManager = userContext.getSystemService(CaptioningManager.class); + mCaptioningManager.set(userContext.getSystemService(CaptioningManager.class)); } - public boolean areCaptionsEnabled() { - return mCaptioningManager.isSystemAudioCaptioningEnabled(); + public void getCaptionsEnabledState(boolean checkForSwitchState) { + mWorker.obtainMessage(W.GET_CAPTIONS_ENABLED_STATE, checkForSwitchState).sendToTarget(); } - public void setCaptionsEnabled(boolean isEnabled) { - mCaptioningManager.setSystemAudioCaptioningEnabled(isEnabled); + public void setCaptionsEnabledState(boolean enabled) { + mWorker.obtainMessage(W.SET_CAPTIONS_ENABLED_STATE, enabled).sendToTarget(); } public void getCaptionsComponentState(boolean fromTooltip) { @@ -386,8 +385,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void setEnableDialogs(boolean volumeUi, boolean safetyWarning) { - mShowVolumeDialog = volumeUi; - mShowSafetyWarning = safetyWarning; + mShowVolumeDialog = volumeUi; + mShowSafetyWarning = safetyWarning; } @Override @@ -438,12 +437,38 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } private void onShowCsdWarningW(@AudioManager.CsdWarning int csdWarning, int durationMs) { - mCallbacks.onShowCsdWarning(csdWarning, durationMs); + mCallbacks.onShowCsdWarning(csdWarning, durationMs); } private void onGetCaptionsComponentStateW(boolean fromTooltip) { - mCallbacks.onCaptionComponentStateChanged( - mCaptioningManager.isSystemAudioCaptioningUiEnabled(), fromTooltip); + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + mCallbacks.onCaptionComponentStateChanged( + captioningManager.isSystemAudioCaptioningUiEnabled(), fromTooltip); + } else { + Log.e(TAG, "onGetCaptionsComponentStateW(), null captioningManager"); + } + } + + private void onGetCaptionsEnabledStateW(boolean checkForSwitchState) { + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + mCallbacks.onCaptionEnabledStateChanged( + captioningManager.isSystemAudioCaptioningEnabled(), checkForSwitchState); + } else { + Log.e(TAG, "onGetCaptionsEnabledStateW(), null captioningManager"); + } + } + + private void onSetCaptionsEnabledStateW(boolean enabled) { + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + captioningManager.setSystemAudioCaptioningEnabled(enabled); + mCallbacks.onCaptionEnabledStateChanged( + captioningManager.isSystemAudioCaptioningEnabled(), false); + } else { + Log.e(TAG, "onGetCaptionsEnabledStateW(), null captioningManager"); + } } private void onAccessibilityModeChanged(Boolean showA11yStream) { @@ -822,6 +847,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static final int ACCESSIBILITY_MODE_CHANGED = 15; private static final int GET_CAPTIONS_COMPONENT_STATE = 16; private static final int SHOW_CSD_WARNING = 17; + private static final int GET_CAPTIONS_ENABLED_STATE = 18; + private static final int SET_CAPTIONS_ENABLED_STATE = 19; W(Looper looper) { super(looper); @@ -849,6 +876,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa case ACCESSIBILITY_MODE_CHANGED: onAccessibilityModeChanged((Boolean) msg.obj); break; case SHOW_CSD_WARNING: onShowCsdWarningW(msg.arg1, msg.arg2); break; + case GET_CAPTIONS_ENABLED_STATE: + onGetCaptionsEnabledStateW((Boolean) msg.obj); break; + case SET_CAPTIONS_ENABLED_STATE: + onSetCaptionsEnabledStateW((Boolean) msg.obj); break; } } } @@ -1017,6 +1048,17 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa componentEnabled, fromTooltip)); } } + + @Override + public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch) { + boolean captionsEnabled = isEnabled != null && isEnabled; + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post( + () -> entry.getKey().onCaptionEnabledStateChanged( + captionsEnabled, checkBeforeSwitch)); + } + } + } private final class RingerModeObservers { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 0e97e2192cdc..dcc0525bb436 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1333,21 +1333,30 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (!isServiceComponentEnabled) return; - updateCaptionsIcon(); + checkEnabledStateForCaptionsIconUpdate(); if (fromTooltip) showCaptionsTooltip(); } - private void updateCaptionsIcon() { - boolean captionsEnabled = mController.areCaptionsEnabled(); - if (mODICaptionsIcon.getCaptionsEnabled() != captionsEnabled) { - mHandler.post(mODICaptionsIcon.setCaptionsEnabled(captionsEnabled)); + private void updateCaptionsEnabledH(boolean isCaptionsEnabled, boolean checkForSwitchState) { + if (checkForSwitchState) { + mController.setCaptionsEnabledState(!isCaptionsEnabled); + } else { + updateCaptionsIcon(isCaptionsEnabled); + } + } + + private void checkEnabledStateForCaptionsIconUpdate() { + mController.getCaptionsEnabledState(false); + } + + private void updateCaptionsIcon(boolean isCaptionsEnabled) { + if (mODICaptionsIcon.getCaptionsEnabled() != isCaptionsEnabled) { + mHandler.post(mODICaptionsIcon.setCaptionsEnabled(isCaptionsEnabled)); } } private void onCaptionIconClicked() { - boolean isEnabled = mController.areCaptionsEnabled(); - mController.setCaptionsEnabled(!isEnabled); - updateCaptionsIcon(); + mController.getCaptionsEnabledState(true); } private void incrementManualToggleCount() { @@ -2365,7 +2374,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } else { updateRowsH(activeRow); } - } @Override @@ -2373,6 +2381,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, Boolean isComponentEnabled, Boolean fromTooltip) { updateODICaptionsH(isComponentEnabled, fromTooltip); } + + @Override + public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) { + updateCaptionsEnabledH(isEnabled, checkForSwitchState); + } }; @VisibleForTesting void onPostureChanged(int posture) { diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 9dca013f8aa4..aea3030967d2 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -109,6 +109,7 @@ public class ImageWallpaper extends WallpaperService { private WallpaperManager mWallpaperManager; private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor; private SurfaceHolder mSurfaceHolder; + private boolean mDrawn = false; @VisibleForTesting static final int MIN_SURFACE_WIDTH = 128; @VisibleForTesting @@ -239,6 +240,7 @@ public class ImageWallpaper extends WallpaperService { private void drawFrameSynchronized() { synchronized (mLock) { + if (mDrawn) return; drawFrameInternal(); } } @@ -276,6 +278,7 @@ public class ImageWallpaper extends WallpaperService { Rect dest = mSurfaceHolder.getSurfaceFrame(); try { canvas.drawBitmap(bitmap, null, dest, null); + mDrawn = true; } finally { surface.unlockCanvasAndPost(canvas); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index 677d3ff3df69..c894d914bfa3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -180,8 +180,9 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { } @Test - public void testResume() { - mKeyguardAbsKeyInputViewController.onResume(KeyguardSecurityView.VIEW_REVEALED); + public void testOnViewAttached() { + reset(mLockPatternUtils); + mKeyguardAbsKeyInputViewController.onViewAttached(); verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java index b3496967f525..438f0f43acb6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java @@ -18,6 +18,8 @@ package com.android.keyguard; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; +import static com.android.systemui.flags.Flags.ENABLE_CLOCK_KEYGUARD_PRESENTATION; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -37,6 +39,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -59,8 +62,13 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { @Mock private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; @Mock + private ConnectedDisplayKeyguardPresentation.Factory + mConnectedDisplayKeyguardPresentationFactory; + @Mock private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation; @Mock + private ConnectedDisplayKeyguardPresentation mConnectedDisplayKeyguardPresentation; + @Mock private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper; @Mock private KeyguardStateController mKeyguardStateController; @@ -69,7 +77,7 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { private Executor mBackgroundExecutor = Runnable::run; private KeyguardDisplayManager mManager; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); - + private FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags(); // The default and secondary displays are both in the default group private Display mDefaultDisplay; private Display mSecondaryDisplay; @@ -80,10 +88,14 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + mFakeFeatureFlags.set(ENABLE_CLOCK_KEYGUARD_PRESENTATION, false); mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController, mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor, - mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController)); + mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController, + mConnectedDisplayKeyguardPresentationFactory, mFakeFeatureFlags)); doReturn(mKeyguardPresentation).when(mManager).createPresentation(any()); + doReturn(mConnectedDisplayKeyguardPresentation).when( + mConnectedDisplayKeyguardPresentationFactory).create(any()); mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY, new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS); @@ -138,4 +150,15 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { when(mKeyguardStateController.isOccluded()).thenReturn(true); verify(mManager, never()).createPresentation(eq(mSecondaryDisplay)); } + + @Test + public void testShow_withClockPresentationFlagEnabled_presentationCreated() { + when(mManager.createPresentation(any())).thenCallRealMethod(); + mFakeFeatureFlags.set(ENABLE_CLOCK_KEYGUARD_PRESENTATION, true); + mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay}); + + mManager.show(); + + verify(mConnectedDisplayKeyguardPresentationFactory).create(eq(mSecondaryDisplay)); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index 1a9260c2ede6..3a9473085583 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -20,6 +20,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.inputmethod.InputMethodManager import android.widget.EditText +import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils @@ -29,6 +30,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -36,6 +38,7 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` @@ -45,104 +48,109 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPasswordViewControllerTest : SysuiTestCase() { - @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView - @Mock private lateinit var passwordEntry: EditText - @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode - @Mock lateinit var lockPatternUtils: LockPatternUtils - @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback - @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory - @Mock lateinit var latencyTracker: LatencyTracker - @Mock lateinit var inputMethodManager: InputMethodManager - @Mock lateinit var emergencyButtonController: EmergencyButtonController - @Mock lateinit var mainExecutor: DelayableExecutor - @Mock lateinit var falsingCollector: FalsingCollector - @Mock lateinit var keyguardViewController: KeyguardViewController - @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView + @Mock private lateinit var passwordEntry: EditText + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode + @Mock lateinit var lockPatternUtils: LockPatternUtils + @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback + @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock lateinit var latencyTracker: LatencyTracker + @Mock lateinit var inputMethodManager: InputMethodManager + @Mock lateinit var emergencyButtonController: EmergencyButtonController + @Mock lateinit var mainExecutor: DelayableExecutor + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController + private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - Mockito.`when`( - keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>( - R.id.bouncer_message_area)) - .thenReturn(mKeyguardMessageArea) - Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) - .thenReturn(mKeyguardMessageAreaController) - Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) - Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) - .thenReturn(passwordEntry) - `when`(keyguardPasswordView.resources).thenReturn(context.resources) - val fakeFeatureFlags = FakeFeatureFlags() - fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) - keyguardPasswordViewController = - KeyguardPasswordViewController( - keyguardPasswordView, - keyguardUpdateMonitor, - securityMode, - lockPatternUtils, - keyguardSecurityCallback, - messageAreaControllerFactory, - latencyTracker, - inputMethodManager, - emergencyButtonController, - mainExecutor, - mContext.resources, - falsingCollector, - keyguardViewController, - fakeFeatureFlags) - } + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + Mockito.`when`( + keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>( + R.id.bouncer_message_area + ) + ) + .thenReturn(mKeyguardMessageArea) + Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) + .thenReturn(mKeyguardMessageAreaController) + Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) + Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) + .thenReturn(passwordEntry) + whenever(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button)) + .thenReturn(mock(ImageView::class.java)) + `when`(keyguardPasswordView.resources).thenReturn(context.resources) + val fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + keyguardPasswordViewController = + KeyguardPasswordViewController( + keyguardPasswordView, + keyguardUpdateMonitor, + securityMode, + lockPatternUtils, + keyguardSecurityCallback, + messageAreaControllerFactory, + latencyTracker, + inputMethodManager, + emergencyButtonController, + mainExecutor, + mContext.resources, + falsingCollector, + keyguardViewController, + fakeFeatureFlags + ) + } - @Test - fun testFocusWhenBouncerIsShown() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - keyguardPasswordView.post { - verify(keyguardPasswordView).requestFocus() - verify(keyguardPasswordView).showKeyboard() + @Test + fun testFocusWhenBouncerIsShown() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + keyguardPasswordView.post { + verify(keyguardPasswordView).requestFocus() + verify(keyguardPasswordView).showKeyboard() + } } - } - @Test - fun testDoNotFocusWhenBouncerIsHidden() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - verify(keyguardPasswordView, never()).requestFocus() - } + @Test + fun testDoNotFocusWhenBouncerIsHidden() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + verify(keyguardPasswordView, never()).requestFocus() + } - @Test - fun testHideKeyboardWhenOnPause() { - keyguardPasswordViewController.onPause() - keyguardPasswordView.post { - verify(keyguardPasswordView).clearFocus() - verify(keyguardPasswordView).hideKeyboard() + @Test + fun testHideKeyboardWhenOnPause() { + keyguardPasswordViewController.onPause() + keyguardPasswordView.post { + verify(keyguardPasswordView).clearFocus() + verify(keyguardPasswordView).hideKeyboard() + } } - } - @Test - fun startAppearAnimation() { - keyguardPasswordViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false) - } + @Test + fun testOnViewAttached() { + keyguardPasswordViewController.onViewAttached() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false) + } - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - keyguardPasswordViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) - } + @Test + fun testOnViewAttached_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + keyguardPasswordViewController.onViewAttached() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } - @Test - fun testMessageIsSetWhenReset() { - keyguardPasswordViewController.resetState() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) - } + @Test + fun testMessageIsSetWhenReset() { + keyguardPasswordViewController.resetState() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 9f7ab7b30d19..1acd676f02cd 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -46,6 +46,7 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -54,35 +55,35 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPatternViewControllerTest : SysuiTestCase() { - private lateinit var mKeyguardPatternView: KeyguardPatternView + private lateinit var mKeyguardPatternView: KeyguardPatternView - @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode + @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode - @Mock private lateinit var mLockPatternUtils: LockPatternUtils + @Mock private lateinit var mLockPatternUtils: LockPatternUtils - @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback + @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback - @Mock private lateinit var mLatencyTracker: LatencyTracker - private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() + @Mock private lateinit var mLatencyTracker: LatencyTracker + private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() - @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController + @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController - @Mock - private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock + private lateinit var mKeyguardMessageAreaControllerFactory: + KeyguardMessageAreaController.Factory - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - @Mock private lateinit var mPostureController: DevicePostureController + @Mock private lateinit var mPostureController: DevicePostureController - private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController - private lateinit var fakeFeatureFlags: FakeFeatureFlags + private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController + private lateinit var fakeFeatureFlags: FakeFeatureFlags - @Captor - lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> + @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @Before fun setup() { @@ -91,9 +92,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { .thenReturn(mKeyguardMessageAreaController) fakeFeatureFlags = FakeFeatureFlags() fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false) - mKeyguardPatternView = View.inflate(mContext, R.layout.keyguard_pattern_view, null) - as KeyguardPatternView - + mKeyguardPatternView = + View.inflate(mContext, R.layout.keyguard_pattern_view, null) as KeyguardPatternView mKeyguardPatternViewController = KeyguardPatternViewController( @@ -125,8 +125,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { @Test fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() { overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f) - whenever(mPostureController.devicePosture) - .thenReturn(DEVICE_POSTURE_HALF_OPENED) + whenever(mPostureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) mKeyguardPatternViewController.onViewAttached() @@ -159,39 +158,37 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio) } - @Test - fun withFeatureFlagOn_oldMessage_isHidden() { - fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) - - mKeyguardPatternViewController.onViewAttached() - - verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable() - } - - @Test - fun onPause_resetsText() { - mKeyguardPatternViewController.init() - mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) - } - - @Test - fun startAppearAnimation() { - mKeyguardPatternViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false) - } - - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - mKeyguardPatternViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) - } - - @Test - fun resume() { - mKeyguardPatternViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) - } + @Test + fun withFeatureFlagOn_oldMessage_isHidden() { + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + + mKeyguardPatternViewController.onViewAttached() + + verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable() + } + + @Test + fun onPause_resetsText() { + mKeyguardPatternViewController.init() + mKeyguardPatternViewController.onPause() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) + } + + @Test + fun testOnViewAttached() { + reset(mKeyguardMessageAreaController) + reset(mLockPatternUtils) + mKeyguardPatternViewController.onViewAttached() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false) + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) + } + + @Test + fun testOnViewAttached_withExistingMessage() { + reset(mKeyguardMessageAreaController) + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + mKeyguardPatternViewController.onViewAttached() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index cf86c2192352..efe1955595ca 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -100,6 +100,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { .thenReturn(mDeleteButton); when(mPinBasedInputView.findViewById(R.id.key_enter)) .thenReturn(mOkButton); + + when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources()); FakeFeatureFlags featureFlags = new FakeFeatureFlags(); featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 309d9e084ac8..80fd7213778e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -185,27 +185,27 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { } @Test - fun startAppearAnimation() { + fun testOnViewAttached() { val pinViewController = constructPinViewController(mockKeyguardPinView) - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(keyguardMessageAreaController) .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test - fun startAppearAnimation_withExistingMessage() { + fun testOnViewAttached_withExistingMessage() { val pinViewController = constructPinViewController(mockKeyguardPinView) Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean()) } @Test - fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { + fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) @@ -213,7 +213,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3) `when`(passwordTextView.text).thenReturn("") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(deleteButton).visibility = View.INVISIBLE verify(enterButton).visibility = View.INVISIBLE @@ -222,7 +222,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { } @Test - fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { + fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) @@ -230,7 +230,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6) `when`(passwordTextView.text).thenReturn("") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(deleteButton).visibility = View.VISIBLE verify(enterButton).visibility = View.VISIBLE diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 0192e78dc09d..ad4bd584b5fc 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -58,12 +58,15 @@ import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings @@ -133,6 +136,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var userInteractor: UserInteractor @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Captor private lateinit var swipeListenerArgumentCaptor: @@ -182,6 +186,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { whenever(keyguardPasswordView.windowInsetsController).thenReturn(windowInsetsController) whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN) whenever(keyguardStateController.canDismissLockScreen()).thenReturn(true) + whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true) featureFlags = FakeFeatureFlags() featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) @@ -249,6 +254,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mock(), { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, userInteractor, + deviceProvisionedController, faceAuthAccessibilityDelegate, keyguardTransitionInteractor ) { @@ -506,6 +512,30 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // THEN the next security method of None will dismiss keyguard. verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) } + @Test + fun showNextSecurityScreenOrFinish_SimPin_Swipe_userNotSetup() { + // GIVEN the current security method is SimPin + whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) + whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) + .thenReturn(false) + underTest.showSecurityScreen(SecurityMode.SimPin) + + // WHEN a request is made from the SimPin screens to show the next security method + whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) + .thenReturn(SecurityMode.None) + // WHEN security method is SWIPE + whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) + whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(false) + underTest.showNextSecurityScreenOrFinish( + /* authenticated= */ true, + TARGET_USER_ID, + /* bypassSecondaryLockScreen= */ true, + SecurityMode.SimPin + ) + + // THEN the next security method of None will dismiss keyguard. + verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt()) + } @Test fun onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { @@ -578,18 +608,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) - clearInvocations(viewFlipperController) configurationListenerArgumentCaptor.value.onThemeChanged() - verify(viewFlipperController).clearViews() - verify(viewFlipperController) - .asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(), - onViewInflatedCallbackArgumentCaptor.capture() - ) - onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) - verify(view).reset() - verify(viewFlipperController).reset() verify(view).reloadColors() } @@ -599,16 +618,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) - clearInvocations(viewFlipperController) configurationListenerArgumentCaptor.value.onUiModeChanged() - verify(viewFlipperController).clearViews() - verify(viewFlipperController) - .asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(), - onViewInflatedCallbackArgumentCaptor.capture() - ) - onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(view).reloadColors() } @@ -847,6 +857,17 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(userSwitcher).setAlpha(0f) } + @Test + fun testOnUserSwitched() { + val userSwitchCallbackArgumentCaptor = + argumentCaptor<UserSwitcherController.UserSwitchCallback>() + underTest.onViewAttached() + verify(userSwitcherController) + .addUserSwitchCallback(capture(userSwitchCallbackArgumentCaptor)) + userSwitchCallbackArgumentCaptor.value.onUserSwitched() + verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any()) + } + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index a3acc781f2a7..291dda256c4f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -97,6 +97,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Test fun onViewAttached() { underTest.onViewAttached() + verify(keyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test @@ -120,8 +122,6 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { underTest.startAppearAnimation() - verify(keyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index efcf4ddb5c71..626faa601970 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -98,6 +98,8 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { underTest.onViewAttached() Mockito.verify(keyguardUpdateMonitor) .registerCallback(any(KeyguardUpdateMonitorCallback::class.java)) + Mockito.verify(keyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test @@ -120,8 +122,6 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { underTest.startAppearAnimation() - Mockito.verify(keyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java index 362d26b040e8..cf4e2c319ac9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; @@ -86,6 +88,9 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { @Test public void testFingerprintReEnrollDialog_onRemovalSucceeded() { + assumeTrue(getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)); + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, BiometricSourceType.FINGERPRINT); @@ -109,6 +114,9 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { @Test public void testFingerprintReEnrollDialog_onRemovalError() { + assumeTrue(getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)); + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, BiometricSourceType.FINGERPRINT); @@ -130,6 +138,9 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { @Test public void testFaceReEnrollDialog_onRemovalSucceeded() { + assumeTrue(getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_FACE)); + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, BiometricSourceType.FACE); @@ -153,6 +164,9 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { @Test public void testFaceReEnrollDialog_onRemovalError() { + assumeTrue(getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_FACE)); + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, BiometricSourceType.FACE); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt index efae3fe1af2c..8eb274a65bc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt @@ -24,13 +24,12 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT -import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -39,13 +38,12 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class BouncerMessageFactoryTest : SysuiTestCase() { private lateinit var underTest: BouncerMessageFactory - @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository @Mock private lateinit var securityModel: KeyguardSecurityModel @@ -55,7 +53,8 @@ class BouncerMessageFactoryTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) testScope = TestScope() - underTest = BouncerMessageFactory(updateMonitor, securityModel) + biometricSettingsRepository = FakeBiometricSettingsRepository() + underTest = BouncerMessageFactory(biometricSettingsRepository, securityModel) } @Test @@ -167,7 +166,7 @@ class BouncerMessageFactoryTest : SysuiTestCase() { secondaryMessageOverride: String? = null, ): BouncerMessageModel? { whenever(securityModel.getSecurityMode(0)).thenReturn(mode) - whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(fpAuthAllowed) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(fpAuthAllowed) return underTest.createFromPromptReason( reason, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt index 2be7d8a43a18..562a8ef512d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt @@ -101,14 +101,15 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository() testScope = TestScope() - whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) underTest = BouncerMessageRepositoryImpl( trustRepository = trustRepository, biometricSettingsRepository = biometricSettingsRepository, updateMonitor = updateMonitor, - bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel), + bouncerMessageFactory = + BouncerMessageFactory(biometricSettingsRepository, securityModel), userRepository = userRepository, fingerprintAuthRepository = fingerprintRepository, systemPropertiesHelper = systemPropertiesHelper @@ -222,8 +223,7 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { whenever(systemPropertiesHelper.get("sys.boot.reason.last")) .thenReturn("reboot,mainline_update") userRepository.setSelectedUserInfo(PRIMARY_USER) - biometricSettingsRepository.setFaceEnrolled(true) - biometricSettingsRepository.setIsFaceAuthEnabled(true) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) verifyMessagesForAuthFlag( STRONG_AUTH_REQUIRED_AFTER_BOOT to @@ -236,8 +236,8 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { testScope.runTest { userRepository.setSelectedUserInfo(PRIMARY_USER) trustRepository.setCurrentUserTrustManaged(false) - biometricSettingsRepository.setFaceEnrolled(false) - biometricSettingsRepository.setFingerprintEnrolled(false) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) verifyMessagesForAuthFlag( STRONG_AUTH_NOT_REQUIRED to null, @@ -258,8 +258,8 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { fun authFlagsChanges_withTrustManaged_providesDifferentMessages() = testScope.runTest { userRepository.setSelectedUserInfo(PRIMARY_USER) - biometricSettingsRepository.setFaceEnrolled(false) - biometricSettingsRepository.setFingerprintEnrolled(false) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) trustRepository.setCurrentUserTrustManaged(true) @@ -290,10 +290,9 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { testScope.runTest { userRepository.setSelectedUserInfo(PRIMARY_USER) trustRepository.setCurrentUserTrustManaged(false) - biometricSettingsRepository.setFingerprintEnrolled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) - biometricSettingsRepository.setIsFaceAuthEnabled(true) - biometricSettingsRepository.setFaceEnrolled(true) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) verifyMessagesForAuthFlag( STRONG_AUTH_NOT_REQUIRED to null, @@ -320,11 +319,9 @@ class BouncerMessageRepositoryTest : SysuiTestCase() { testScope.runTest { userRepository.setSelectedUserInfo(PRIMARY_USER) trustRepository.setCurrentUserTrustManaged(false) - biometricSettingsRepository.setIsFaceAuthEnabled(false) - biometricSettingsRepository.setFaceEnrolled(false) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) - biometricSettingsRepository.setFingerprintEnrolled(true) - biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) verifyMessagesForAuthFlag( STRONG_AUTH_NOT_REQUIRED to null, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt index 38e5728f6e70..0d172706b127 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt @@ -94,9 +94,9 @@ class AlternateBouncerInteractorTest : SysuiTestCase() { } @Test - fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() { + fun canShowAlternateBouncerForFingerprint_ifFingerprintIsNotUsuallyAllowed() { givenCanShowAlternateBouncer() - biometricSettingsRepository.setFingerprintEnrolled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) assertFalse(underTest.canShowAlternateBouncerForFingerprint()) } @@ -104,15 +104,7 @@ class AlternateBouncerInteractorTest : SysuiTestCase() { @Test fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() { givenCanShowAlternateBouncer() - biometricSettingsRepository.setStrongBiometricAllowed(false) - - assertFalse(underTest.canShowAlternateBouncerForFingerprint()) - } - - @Test - fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() { - givenCanShowAlternateBouncer() - biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(false) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) assertFalse(underTest.canShowAlternateBouncerForFingerprint()) } @@ -189,14 +181,13 @@ class AlternateBouncerInteractorTest : SysuiTestCase() { private fun givenCanShowAlternateBouncer() { bouncerRepository.setAlternateBouncerUIAvailable(true) - biometricSettingsRepository.setFingerprintEnrolled(true) - biometricSettingsRepository.setStrongBiometricAllowed(true) - biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false) whenever(keyguardStateController.isUnlocked).thenReturn(false) } private fun givenCannotShowAlternateBouncer() { - biometricSettingsRepository.setFingerprintEnrolled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 3ca94aa8e7af..4089abef399b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -22,9 +22,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.R.string.keyguard_enter_pin import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.R.string.kg_unlock_with_pin_or_fp import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.factory.BouncerMessageFactory import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository @@ -34,11 +33,11 @@ import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -49,14 +48,13 @@ import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class BouncerMessageInteractorTest : SysuiTestCase() { @Mock private lateinit var securityModel: KeyguardSecurityModel - @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback> private lateinit var underTest: BouncerMessageInteractor @@ -73,10 +71,11 @@ class BouncerMessageInteractorTest : SysuiTestCase() { userRepository.setUserInfos(listOf(PRIMARY_USER)) testScope = TestScope() countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java) + biometricSettingsRepository = FakeBiometricSettingsRepository() allowTestableLooperAsMainThread() whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) - whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) } suspend fun TestScope.init() { @@ -86,7 +85,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { underTest = BouncerMessageInteractor( repository = repository, - factory = BouncerMessageFactory(updateMonitor, securityModel), + factory = BouncerMessageFactory(biometricSettingsRepository, securityModel), userRepository = userRepository, countDownTimerUtil = countDownTimerUtil, featureFlags = featureFlags @@ -151,7 +150,8 @@ class BouncerMessageInteractorTest : SysuiTestCase() { underTest.setCustomMessage("not empty") val customMessage = repository.customMessage - assertThat(customMessage.value!!.message!!.messageResId).isEqualTo(keyguard_enter_pin) + assertThat(customMessage.value!!.message!!.messageResId) + .isEqualTo(kg_unlock_with_pin_or_fp) assertThat(customMessage.value!!.secondaryMessage!!.message).isEqualTo("not empty") underTest.setCustomMessage(null) @@ -168,7 +168,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { val faceAcquisitionMessage = repository.faceAcquisitionMessage assertThat(faceAcquisitionMessage.value!!.message!!.messageResId) - .isEqualTo(keyguard_enter_pin) + .isEqualTo(kg_unlock_with_pin_or_fp) assertThat(faceAcquisitionMessage.value!!.secondaryMessage!!.message) .isEqualTo("not empty") @@ -186,7 +186,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { val fingerprintAcquisitionMessage = repository.fingerprintAcquisitionMessage assertThat(fingerprintAcquisitionMessage.value!!.message!!.messageResId) - .isEqualTo(keyguard_enter_pin) + .isEqualTo(kg_unlock_with_pin_or_fp) assertThat(fingerprintAcquisitionMessage.value!!.secondaryMessage!!.message) .isEqualTo("not empty") @@ -275,7 +275,8 @@ class BouncerMessageInteractorTest : SysuiTestCase() { repository.setBiometricLockedOutMessage(null) // sets the default message if everything else is null - assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin) + assertThat(bouncerMessage()!!.message!!.messageResId) + .isEqualTo(kg_unlock_with_pin_or_fp) } private fun message(value: String): BouncerMessageModel { diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java index 7628be44755d..662c89c268e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java @@ -80,6 +80,7 @@ public class IntentCreatorTest extends SysuiTestCase { assertEquals(Intent.ACTION_EDIT, intent.getAction()); assertEquals("image/*", intent.getType()); assertEquals(null, intent.getComponent()); + assertEquals("clipboard", intent.getStringExtra("edit_source")); assertFlags(intent, EXTERNAL_INTENT_FLAGS); // try again with an editor component diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 692d794f21f0..8416c46a3f38 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -18,12 +18,14 @@ package com.android.systemui.controls.ui import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner +import android.view.HapticFeedbackConstants import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.controls.settings.FakeControlsSettingsRepository -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController @@ -33,6 +35,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyBoolean @@ -68,8 +71,6 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { @Mock private lateinit var metricsLogger: ControlsMetricsLogger @Mock - private lateinit var featureFlags: FeatureFlags - @Mock private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager companion object { @@ -82,6 +83,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { private lateinit var action: ControlActionCoordinatorImpl.Action private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + private val featureFlags = FakeFeatureFlags() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -101,6 +104,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { metricsLogger, vibratorHelper, controlsSettingsRepository, + featureFlags )) coordinator.activityContext = mContext @@ -194,4 +198,50 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { verify(coordinator).bouncerOrRun(action) verify(action, never()).invoke() } + + @Test + fun drag_isEdge_oneWayHapticsDisabled_usesVibrate() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) + + coordinator.drag(cvh, true) + + verify(vibratorHelper).vibrate( + Vibrations.rangeEdgeEffect + ) + } + + @Test + fun drag_isNotEdge_oneWayHapticsDisabled_usesVibrate() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) + + coordinator.drag(cvh, false) + + verify(vibratorHelper).vibrate( + Vibrations.rangeMiddleEffect + ) + } + + @Test + fun drag_isEdge_oneWayHapticsEnabled_usesPerformHapticFeedback() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + + coordinator.drag(cvh, true) + + verify(vibratorHelper).performHapticFeedback( + any(), + eq(HapticFeedbackConstants.SEGMENT_TICK) + ) + } + + @Test + fun drag_isNotEdge_oneWayHapticsEnabled_usesPerformHapticFeedback() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + + coordinator.drag(cvh, false) + + verify(vibratorHelper).performHapticFeedback( + any(), + eq(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java index 07cb5d88a515..6a178895839b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java @@ -22,17 +22,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import android.os.RemoteException; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; -import com.android.internal.app.AssistUtils; -import com.android.internal.app.IVisualQueryDetectionAttentionListener; import com.android.systemui.SysuiTestCase; -import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; import com.android.systemui.shared.condition.Condition; import org.junit.Before; @@ -50,9 +47,7 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { @Mock Condition.Callback mCallback; @Mock - AssistUtils mAssistUtils; - @Mock - DreamOverlayStateController mDreamOverlayStateController; + AssistManager mAssistManager; @Mock CoroutineScope mScope; @@ -62,55 +57,34 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { public void setup() { MockitoAnnotations.initMocks(this); - mAssistantAttentionCondition = - new AssistantAttentionCondition(mScope, mDreamOverlayStateController, mAssistUtils); + mAssistantAttentionCondition = new AssistantAttentionCondition(mScope, mAssistManager); // Adding a callback also starts the condition. mAssistantAttentionCondition.addCallback(mCallback); } @Test public void testEnableVisualQueryDetection() { - final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - argumentCaptor.getValue().onStateChanged(); - - verify(mAssistUtils).enableVisualQueryDetection(any()); + verify(mAssistManager).addVisualQueryAttentionListener( + any(VisualQueryAttentionListener.class)); } @Test public void testDisableVisualQueryDetection() { - final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - argumentCaptor.getValue().onStateChanged(); - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(false); - argumentCaptor.getValue().onStateChanged(); - - verify(mAssistUtils).disableVisualQueryDetection(); + mAssistantAttentionCondition.stop(); + verify(mAssistManager).removeVisualQueryAttentionListener( + any(VisualQueryAttentionListener.class)); } @Test - public void testAttentionChangedTriggersCondition() throws RemoteException { - final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - callbackCaptor.getValue().onStateChanged(); - - final ArgumentCaptor<IVisualQueryDetectionAttentionListener> listenerCaptor = - ArgumentCaptor.forClass(IVisualQueryDetectionAttentionListener.class); - verify(mAssistUtils).enableVisualQueryDetection(listenerCaptor.capture()); + public void testAttentionChangedTriggersCondition() { + final ArgumentCaptor<VisualQueryAttentionListener> argumentCaptor = + ArgumentCaptor.forClass(VisualQueryAttentionListener.class); + verify(mAssistManager).addVisualQueryAttentionListener(argumentCaptor.capture()); - listenerCaptor.getValue().onAttentionGained(); + argumentCaptor.getValue().onAttentionGained(); assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue(); - listenerCaptor.getValue().onAttentionLost(); + argumentCaptor.getValue().onAttentionLost(); assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse(); verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt new file mode 100644 index 000000000000..632d149c9520 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt @@ -0,0 +1,147 @@ +/* + * 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.keyevent.domain.interactor + +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyEventInteractorTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private lateinit var keyguardInteractorWithDependencies: + KeyguardInteractorFactory.WithDependencies + @Mock private lateinit var keyguardKeyEventInteractor: KeyguardKeyEventInteractor + @Mock private lateinit var backActionInteractor: BackActionInteractor + + private lateinit var underTest: KeyEventInteractor + + @Before + fun setup() { + keyguardInteractorWithDependencies = KeyguardInteractorFactory.create() + underTest = + KeyEventInteractor( + backActionInteractor, + keyguardKeyEventInteractor, + ) + } + + @Test + fun dispatchBackKey_notHandledByKeyguardKeyEventInteractor_handledByBackActionInteractor() { + val backKeyEventActionDown = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK) + val backKeyEventActionUp = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) + + // GIVEN back key ACTION_DOWN and ACTION_UP aren't handled by the keyguardKeyEventInteractor + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionDown)) + .thenReturn(false) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionUp)) + .thenReturn(false) + + // WHEN back key event ACTION_DOWN, the event is handled even though back isn't requested + assertThat(underTest.dispatchKeyEvent(backKeyEventActionDown)).isTrue() + // THEN back event isn't handled on ACTION_DOWN + verify(backActionInteractor, never()).onBackRequested() + + // WHEN back key event ACTION_UP + assertThat(underTest.dispatchKeyEvent(backKeyEventActionUp)).isTrue() + // THEN back event is handled on ACTION_UP + verify(backActionInteractor).onBackRequested() + } + + @Test + fun dispatchKeyEvent_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(false) + assertThat(underTest.dispatchKeyEvent(keyEvent)).isFalse() + } + + @Test + fun dispatchKeyEvent_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(true) + assertThat(underTest.dispatchKeyEvent(keyEvent)).isTrue() + } + + @Test + fun interceptMediaKey_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(false) + assertThat(underTest.interceptMediaKey(keyEvent)).isFalse() + } + + @Test + fun interceptMediaKey_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(true) + assertThat(underTest.interceptMediaKey(keyEvent)).isTrue() + } + + @Test + fun dispatchKeyEventPreIme_isNotHandledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(false) + assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isFalse() + } + + @Test + fun dispatchKeyEventPreIme_handledByKeyguardKeyEventInteractor() { + val keyEvent = + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_SPACE, + ) + whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(true) + assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index c6a2fa50b446..a6930d595326 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -36,6 +36,11 @@ import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FaceSensorInfo +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.BiometricType.FACE @@ -63,6 +68,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.isNull import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -89,6 +95,8 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { ArgumentCaptor<IBiometricEnabledOnKeyguardCallback.Stub> private lateinit var userRepository: FakeUserRepository private lateinit var devicePostureRepository: FakeDevicePostureRepository + private lateinit var facePropertyRepository: FakeFacePropertyRepository + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -102,6 +110,8 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { testScope = TestScope(testDispatcher) userRepository = FakeUserRepository() devicePostureRepository = FakeDevicePostureRepository() + facePropertyRepository = FakeFacePropertyRepository() + fingerprintPropertyRepository = FakeFingerprintPropertyRepository() } private suspend fun createBiometricSettingsRepository() { @@ -120,74 +130,110 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { biometricManager = biometricManager, devicePostureRepository = devicePostureRepository, dumpManager = dumpManager, + facePropertyRepository = facePropertyRepository, + fingerprintPropertyRepository = fingerprintPropertyRepository, ) testScope.runCurrent() + fingerprintPropertyRepository.setProperties( + 1, + SensorStrength.STRONG, + FingerprintSensorType.UDFPS_OPTICAL, + emptyMap() + ) verify(lockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture()) + verify(authController, atLeastOnce()).addCallback(authControllerCallback.capture()) } @Test fun fingerprintEnrollmentChange() = testScope.runTest { createBiometricSettingsRepository() - val fingerprintEnrolled = collectLastValue(underTest.isFingerprintEnrolled) + val fingerprintAllowed = collectLastValue(underTest.isFingerprintEnrolledAndEnabled) runCurrent() - verify(authController).addCallback(authControllerCallback.capture()) whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true) enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true) - assertThat(fingerprintEnrolled()).isTrue() + assertThat(fingerprintAllowed()).isTrue() whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false) enrollmentChange(UNDER_DISPLAY_FINGERPRINT, ANOTHER_USER_ID, false) - assertThat(fingerprintEnrolled()).isTrue() + assertThat(fingerprintAllowed()).isTrue() enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, false) - assertThat(fingerprintEnrolled()).isFalse() + assertThat(fingerprintAllowed()).isFalse() } @Test fun strongBiometricAllowedChange() = testScope.runTest { + fingerprintIsEnrolled() + doNotDisableKeyguardAuthFeatures() createBiometricSettingsRepository() - val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed) + + val strongBiometricAllowed by + collectLastValue(underTest.isFingerprintAuthCurrentlyAllowed) runCurrent() onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) - assertThat(strongBiometricAllowed()).isTrue() + assertThat(strongBiometricAllowed).isTrue() onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID) - assertThat(strongBiometricAllowed()).isFalse() + assertThat(strongBiometricAllowed).isFalse() } @Test fun convenienceBiometricAllowedChange() = testScope.runTest { overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false) + deviceIsInPostureThatSupportsFaceAuth() + faceAuthIsEnrolled() + faceAuthIsNonStrongBiometric() createBiometricSettingsRepository() - val convenienceBiometricAllowed = - collectLastValue(underTest.isNonStrongBiometricAllowed) - runCurrent() + val convenienceFaceAuthAllowed by collectLastValue(underTest.isFaceAuthCurrentlyAllowed) + doNotDisableKeyguardAuthFeatures() + faceAuthIsEnabledByBiometricManager() + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) onNonStrongAuthChanged(true, PRIMARY_USER_ID) - assertThat(convenienceBiometricAllowed()).isTrue() + runCurrent() + assertThat(convenienceFaceAuthAllowed).isTrue() onNonStrongAuthChanged(false, ANOTHER_USER_ID) - assertThat(convenienceBiometricAllowed()).isTrue() + assertThat(convenienceFaceAuthAllowed).isTrue() onNonStrongAuthChanged(false, PRIMARY_USER_ID) - assertThat(convenienceBiometricAllowed()).isFalse() + assertThat(convenienceFaceAuthAllowed).isFalse() mContext.orCreateTestableResources.removeOverride( com.android.internal.R.bool.config_strongAuthRequiredOnBoot ) } + private fun faceAuthIsNonStrongBiometric() { + facePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.CONVENIENCE)) + } + + private fun faceAuthIsStrongBiometric() { + facePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) + } + + private fun deviceIsInPostureThatSupportsFaceAuth() { + overrideResource( + R.integer.config_face_auth_supported_posture, + DevicePostureController.DEVICE_POSTURE_FLIPPED + ) + devicePostureRepository.setCurrentPosture(DevicePosture.FLIPPED) + } + @Test fun whenStrongAuthRequiredAfterBoot_nonStrongBiometricNotAllowed() = testScope.runTest { overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, true) createBiometricSettingsRepository() + faceAuthIsNonStrongBiometric() + faceAuthIsEnrolled() + doNotDisableKeyguardAuthFeatures() - val convenienceBiometricAllowed = - collectLastValue(underTest.isNonStrongBiometricAllowed) + val convenienceBiometricAllowed = collectLastValue(underTest.isFaceAuthCurrentlyAllowed) runCurrent() onNonStrongAuthChanged(true, PRIMARY_USER_ID) @@ -201,16 +247,24 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { fun whenStrongBiometricAuthIsNotAllowed_nonStrongBiometrics_alsoNotAllowed() = testScope.runTest { overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false) + faceAuthIsNonStrongBiometric() + deviceIsInPostureThatSupportsFaceAuth() + faceAuthIsEnrolled() createBiometricSettingsRepository() - - val convenienceBiometricAllowed = - collectLastValue(underTest.isNonStrongBiometricAllowed) + doNotDisableKeyguardAuthFeatures() + faceAuthIsEnabledByBiometricManager() runCurrent() + + val convenienceBiometricAllowed by + collectLastValue(underTest.isFaceAuthCurrentlyAllowed) + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) onNonStrongAuthChanged(true, PRIMARY_USER_ID) - assertThat(convenienceBiometricAllowed()).isTrue() + runCurrent() + assertThat(convenienceBiometricAllowed).isTrue() onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, PRIMARY_USER_ID) - assertThat(convenienceBiometricAllowed()).isFalse() + assertThat(convenienceBiometricAllowed).isFalse() mContext.orCreateTestableResources.removeOverride( com.android.internal.R.bool.config_strongAuthRequiredOnBoot ) @@ -229,9 +283,11 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { @Test fun fingerprintDisabledByDpmChange() = testScope.runTest { + fingerprintIsEnrolled(PRIMARY_USER_ID) createBiometricSettingsRepository() + val fingerprintEnabledByDevicePolicy = - collectLastValue(underTest.isFingerprintEnabledByDevicePolicy) + collectLastValue(underTest.isFingerprintEnrolledAndEnabled) runCurrent() whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) @@ -244,43 +300,57 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(fingerprintEnabledByDevicePolicy()).isTrue() } + private fun fingerprintIsEnrolled(userId: Int = PRIMARY_USER_ID) { + whenever(authController.isFingerprintEnrolled(userId)).thenReturn(true) + } + @Test fun faceEnrollmentChangeIsPropagatedForTheCurrentUser() = testScope.runTest { createBiometricSettingsRepository() + faceAuthIsEnabledByBiometricManager() + + doNotDisableKeyguardAuthFeatures(PRIMARY_USER_ID) + runCurrent() clearInvocations(authController) whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false) - val faceEnrolled = collectLastValue(underTest.isFaceEnrolled) + val faceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() verify(authController).addCallback(authControllerCallback.capture()) enrollmentChange(REAR_FINGERPRINT, PRIMARY_USER_ID, true) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() enrollmentChange(SIDE_FINGERPRINT, PRIMARY_USER_ID, true) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true) enrollmentChange(FACE, ANOTHER_USER_ID, true) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() - whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(true) + faceAuthIsEnrolled() enrollmentChange(FACE, PRIMARY_USER_ID, true) - assertThat(faceEnrolled()).isTrue() + assertThat(faceAuthAllowed()).isTrue() } + private fun faceAuthIsEnabledByBiometricManager(userId: Int = PRIMARY_USER_ID) { + verify(biometricManager, atLeastOnce()) + .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) + biometricManagerCallback.value.onChanged(true, userId) + } + @Test fun faceEnrollmentStatusOfNewUserUponUserSwitch() = testScope.runTest { @@ -290,21 +360,26 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false) whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true) - val faceEnrolled = collectLastValue(underTest.isFaceEnrolled) + val faceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) - assertThat(faceEnrolled()).isFalse() + assertThat(faceAuthAllowed()).isFalse() } @Test fun faceEnrollmentChangesArePropagatedAfterUserSwitch() = testScope.runTest { createBiometricSettingsRepository() + verify(biometricManager) + .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) userRepository.setSelectedUserInfo(ANOTHER_USER) + doNotDisableKeyguardAuthFeatures(ANOTHER_USER_ID) + biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) + runCurrent() clearInvocations(authController) - val faceEnrolled = collectLastValue(underTest.isFaceEnrolled) + val faceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) runCurrent() verify(authController).addCallback(authControllerCallback.capture()) @@ -312,12 +387,14 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true) enrollmentChange(FACE, ANOTHER_USER_ID, true) - assertThat(faceEnrolled()).isTrue() + assertThat(faceAuthAllowed()).isTrue() } @Test fun devicePolicyControlsFaceAuthenticationEnabledState() = testScope.runTest { + faceAuthIsEnrolled() + createBiometricSettingsRepository() verify(biometricManager) .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) @@ -325,62 +402,70 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID))) .thenReturn(KEYGUARD_DISABLE_FINGERPRINT or KEYGUARD_DISABLE_FACE) - val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + val isFaceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) runCurrent() broadcastDPMStateChange() - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthAllowed()).isFalse() biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID) runCurrent() - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthAllowed()).isFalse() whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID))) .thenReturn(KEYGUARD_DISABLE_FINGERPRINT) broadcastDPMStateChange() - assertThat(isFaceAuthEnabled()).isTrue() + assertThat(isFaceAuthAllowed()).isTrue() } @Test fun biometricManagerControlsFaceAuthenticationEnabledStatus() = testScope.runTest { + faceAuthIsEnrolled() + createBiometricSettingsRepository() verify(biometricManager) .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID))) .thenReturn(0) broadcastDPMStateChange() - val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + val isFaceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthAllowed()).isFalse() // Value changes for another user biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthAllowed()).isFalse() // Value changes for current user. biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID) - assertThat(isFaceAuthEnabled()).isTrue() + assertThat(isFaceAuthAllowed()).isTrue() } + private fun faceAuthIsEnrolled(userId: Int = PRIMARY_USER_ID) { + whenever(authController.isFaceAuthEnrolled(userId)).thenReturn(true) + } + @Test fun userChange_biometricEnabledChange_handlesRaceCondition() = testScope.runTest { createBiometricSettingsRepository() + whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true) + verify(biometricManager) .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) - val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + val isFaceAuthAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) runCurrent() userRepository.setSelectedUserInfo(ANOTHER_USER) runCurrent() - assertThat(isFaceAuthEnabled()).isTrue() + assertThat(isFaceAuthAllowed()).isTrue() } @Test @@ -388,9 +473,9 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { testScope.runTest { createBiometricSettingsRepository() - collectLastValue(underTest.isFaceAuthenticationEnabled)() - collectLastValue(underTest.isFaceAuthenticationEnabled)() - collectLastValue(underTest.isFaceAuthenticationEnabled)() + collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)() + collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)() + collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)() verify(biometricManager, times(1)).registerEnabledOnKeyguardCallback(any()) } @@ -495,10 +580,138 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(authFlags()!!.flag).isEqualTo(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) } + @Test + fun faceAuthCurrentlyAllowed_dependsOnStrongAuthBiometricSetting_ifFaceIsClass3() = + testScope.runTest { + createBiometricSettingsRepository() + val isFaceAuthCurrentlyAllowed by collectLastValue(underTest.isFaceAuthCurrentlyAllowed) + + faceAuthIsEnrolled() + deviceIsInPostureThatSupportsFaceAuth() + doNotDisableKeyguardAuthFeatures() + faceAuthIsStrongBiometric() + faceAuthIsEnabledByBiometricManager() + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + onNonStrongAuthChanged(false, PRIMARY_USER_ID) + + assertThat(isFaceAuthCurrentlyAllowed).isTrue() + + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, PRIMARY_USER_ID) + onNonStrongAuthChanged(true, PRIMARY_USER_ID) + + assertThat(isFaceAuthCurrentlyAllowed).isFalse() + } + + @Test + fun faceAuthCurrentlyAllowed_dependsOnNonStrongAuthBiometricSetting_ifFaceIsNotStrong() = + testScope.runTest { + createBiometricSettingsRepository() + val isFaceAuthCurrentlyAllowed by collectLastValue(underTest.isFaceAuthCurrentlyAllowed) + + faceAuthIsEnrolled() + deviceIsInPostureThatSupportsFaceAuth() + doNotDisableKeyguardAuthFeatures() + faceAuthIsNonStrongBiometric() + faceAuthIsEnabledByBiometricManager() + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + onNonStrongAuthChanged(false, PRIMARY_USER_ID) + + assertThat(isFaceAuthCurrentlyAllowed).isFalse() + + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, PRIMARY_USER_ID) + onNonStrongAuthChanged(true, PRIMARY_USER_ID) + + assertThat(isFaceAuthCurrentlyAllowed).isFalse() + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + onNonStrongAuthChanged(true, PRIMARY_USER_ID) + + assertThat(isFaceAuthCurrentlyAllowed).isTrue() + } + + @Test + fun fpAuthCurrentlyAllowed_dependsOnNonStrongAuthBiometricSetting_ifFpIsNotStrong() = + testScope.runTest { + createBiometricSettingsRepository() + val isFingerprintCurrentlyAllowed by + collectLastValue(underTest.isFingerprintAuthCurrentlyAllowed) + + fingerprintIsEnrolled(PRIMARY_USER_ID) + enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true) + doNotDisableKeyguardAuthFeatures(PRIMARY_USER_ID) + runCurrent() + + fingerprintPropertyRepository.setProperties( + 1, + SensorStrength.STRONG, + FingerprintSensorType.UDFPS_OPTICAL, + emptyMap() + ) + // Non strong auth is not allowed now, FP is marked strong + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + onNonStrongAuthChanged(false, PRIMARY_USER_ID) + + assertThat(isFingerprintCurrentlyAllowed).isTrue() + + fingerprintPropertyRepository.setProperties( + 1, + SensorStrength.CONVENIENCE, + FingerprintSensorType.UDFPS_OPTICAL, + emptyMap() + ) + assertThat(isFingerprintCurrentlyAllowed).isFalse() + + fingerprintPropertyRepository.setProperties( + 1, + SensorStrength.WEAK, + FingerprintSensorType.UDFPS_OPTICAL, + emptyMap() + ) + assertThat(isFingerprintCurrentlyAllowed).isFalse() + } + + @Test + fun fpAuthCurrentlyAllowed_dependsOnStrongAuthBiometricSetting_ifFpIsStrong() = + testScope.runTest { + createBiometricSettingsRepository() + val isFingerprintCurrentlyAllowed by + collectLastValue(underTest.isFingerprintAuthCurrentlyAllowed) + + fingerprintIsEnrolled(PRIMARY_USER_ID) + enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true) + doNotDisableKeyguardAuthFeatures(PRIMARY_USER_ID) + runCurrent() + + fingerprintPropertyRepository.setProperties( + 1, + SensorStrength.STRONG, + FingerprintSensorType.UDFPS_OPTICAL, + emptyMap() + ) + // Non strong auth is not allowed now, FP is marked strong + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, PRIMARY_USER_ID) + onNonStrongAuthChanged(true, PRIMARY_USER_ID) + + assertThat(isFingerprintCurrentlyAllowed).isFalse() + + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + onNonStrongAuthChanged(false, PRIMARY_USER_ID) + + assertThat(isFingerprintCurrentlyAllowed).isTrue() + } + private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) { authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled) } + private fun doNotDisableKeyguardAuthFeatures(userId: Int = PRIMARY_USER_ID) { + whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(userId))) + .thenReturn(0) + broadcastDPMStateChange() + } + private fun broadcastDPMStateChange() { fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index fe5b8120428d..64b94707da57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -40,9 +40,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.data.repository.FaceSensorInfo import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository -import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.coroutines.FlowValue @@ -255,7 +253,6 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthBuffer, keyguardTransitionInteractor, featureFlags, - fakeFacePropertyRepository, dumpManager, ) } @@ -525,15 +522,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun authenticateDoesNotRunIfFaceIsNotEnrolled() = + fun authenticateDoesNotRunIfFaceIsNotUsuallyAllowed() = testScope.runTest { - testGatingCheckForFaceAuth { biometricSettingsRepository.setFaceEnrolled(false) } - } - - @Test - fun authenticateDoesNotRunIfFaceIsNotEnabled() = - testScope.runTest { - testGatingCheckForFaceAuth { biometricSettingsRepository.setIsFaceAuthEnabled(false) } + testGatingCheckForFaceAuth { + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + } } @Test @@ -589,21 +582,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun authenticateDoesNotRunWhenNonStrongBiometricIsNotAllowed() = + fun authenticateDoesNotRunWhenFaceAuthIsNotCurrentlyAllowedToRun() = testScope.runTest { testGatingCheckForFaceAuth { - biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) - } - } - - @Test - fun authenticateDoesNotRunWhenStrongBiometricIsNotAllowedAndFaceSensorIsStrong() = - testScope.runTest { - fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) - runCurrent() - - testGatingCheckForFaceAuth(isFaceStrong = true) { - biometricSettingsRepository.setIsStrongBiometricAllowed(false) + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(false) } } @@ -662,7 +644,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { allPreconditionsToRunFaceAuthAreTrue() // Flip one precondition to false. - biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(false) assertThat(canFaceAuthRun()).isFalse() underTest.authenticate( FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, @@ -827,15 +809,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun detectDoesNotRunWhenFaceIsNotEnrolled() = - testScope.runTest { - testGatingCheckForDetect { biometricSettingsRepository.setFaceEnrolled(false) } - } - - @Test - fun detectDoesNotRunWhenFaceIsNotEnabled() = + fun detectDoesNotRunWhenFaceIsNotUsuallyAllowed() = testScope.runTest { - testGatingCheckForDetect { biometricSettingsRepository.setIsFaceAuthEnabled(false) } + testGatingCheckForDetect { + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + } } @Test @@ -932,23 +910,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun detectDoesNotRunWhenNonStrongBiometricIsAllowed() = + fun detectDoesNotRunWhenFaceAuthIsCurrentlyAllowedToRun() = testScope.runTest { testGatingCheckForDetect { - biometricSettingsRepository.setIsNonStrongBiometricAllowed(true) - } - } - - @Test - fun detectDoesNotRunWhenStrongBiometricIsAllowedAndFaceAuthSensorStrengthIsStrong() = - testScope.runTest { - fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) - runCurrent() - - testGatingCheckForDetect(isFaceStrong = true) { - biometricSettingsRepository.setIsStrongBiometricAllowed(true) - // this shouldn't matter as face is set as a strong sensor - biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) } } @@ -1043,12 +1008,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthenticateIsCalled() } - private suspend fun TestScope.testGatingCheckForFaceAuth( - isFaceStrong: Boolean = false, - gatingCheckModifier: () -> Unit - ) { + private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) { initCollectors() - allPreconditionsToRunFaceAuthAreTrue(isFaceStrong) + allPreconditionsToRunFaceAuthAreTrue() gatingCheckModifier() runCurrent() @@ -1057,7 +1019,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { assertThat(underTest.canRunFaceAuth.value).isFalse() // flip the gating check back on. - allPreconditionsToRunFaceAuthAreTrue(isFaceStrong) + allPreconditionsToRunFaceAuthAreTrue() triggerFaceAuth(false) @@ -1076,19 +1038,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthenticateIsNotCalled() } - private suspend fun TestScope.testGatingCheckForDetect( - isFaceStrong: Boolean = false, - gatingCheckModifier: () -> Unit - ) { + private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) { initCollectors() allPreconditionsToRunFaceAuthAreTrue() - if (isFaceStrong) { - biometricSettingsRepository.setStrongBiometricAllowed(false) - } else { - // This will stop face auth from running but is required to be false for detect. - biometricSettingsRepository.setIsNonStrongBiometricAllowed(false) - } + // This will stop face auth from running but is required to be false for detect. + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(false) runCurrent() assertThat(canFaceAuthRun()).isFalse() @@ -1123,13 +1078,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true } } - private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue( - isFaceStrong: Boolean = false - ) { + private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { verify(faceManager, atLeastOnce()) .addLockoutResetCallback(faceLockoutResetCallback.capture()) - biometricSettingsRepository.setFaceEnrolled(true) - biometricSettingsRepository.setIsFaceAuthEnabled(true) underTest.resumeFaceAuth() trustRepository.setCurrentUserTrusted(false) keyguardRepository.setKeyguardGoingAway(false) @@ -1140,14 +1091,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { WakeSleepReason.OTHER ) ) - if (isFaceStrong) { - biometricSettingsRepository.setStrongBiometricAllowed(true) - } else { - biometricSettingsRepository.setIsNonStrongBiometricAllowed(true) - } + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true) + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) biometricSettingsRepository.setIsUserInLockdown(false) fakeUserRepository.setSelectedUserInfo(primaryUser) - biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true) faceLockoutResetCallback.value.onLockoutReset(0) bouncerRepository.setAlternateVisible(true) keyguardRepository.setKeyguardShowing(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt new file mode 100644 index 000000000000..a3f7fc5fc8cf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.media.AudioManager +import android.media.session.MediaSessionLegacyHelper +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.back.domain.interactor.BackActionInteractor +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardKeyEventInteractorTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private val actionDownVolumeDownKeyEvent = + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN) + private val actionDownVolumeUpKeyEvent = + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP) + private val backKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK) + private val awakeWakefulnessMode = + WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + private val asleepWakefulnessMode = + WakefulnessModel(WakefulnessState.ASLEEP, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + + private lateinit var keyguardInteractorWithDependencies: + KeyguardInteractorFactory.WithDependencies + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var shadeController: ShadeController + @Mock private lateinit var mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper + @Mock private lateinit var mediaSessionLegacyHelper: MediaSessionLegacyHelper + @Mock private lateinit var backActionInteractor: BackActionInteractor + + private lateinit var underTest: KeyguardKeyEventInteractor + + @Before + fun setup() { + whenever(mediaSessionLegacyHelperWrapper.getHelper(any())) + .thenReturn(mediaSessionLegacyHelper) + keyguardInteractorWithDependencies = KeyguardInteractorFactory.create() + underTest = + KeyguardKeyEventInteractor( + context, + statusBarStateController, + keyguardInteractorWithDependencies.keyguardInteractor, + statusBarKeyguardViewManager, + shadeController, + mediaSessionLegacyHelperWrapper, + backActionInteractor, + ) + } + + @Test + fun dispatchKeyEvent_volumeKey_dozing_handlesEvents() { + whenever(statusBarStateController.isDozing).thenReturn(true) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isTrue() + verify(mediaSessionLegacyHelper) + .sendVolumeKeyEvent( + eq(actionDownVolumeDownKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isTrue() + verify(mediaSessionLegacyHelper) + .sendVolumeKeyEvent( + eq(actionDownVolumeUpKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + } + + @Test + fun dispatchKeyEvent_volumeKey_notDozing_doesNotHandleEvents() { + whenever(statusBarStateController.isDozing).thenReturn(false) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isFalse() + verify(mediaSessionLegacyHelper, never()) + .sendVolumeKeyEvent( + eq(actionDownVolumeDownKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + + assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isFalse() + verify(mediaSessionLegacyHelper, never()) + .sendVolumeKeyEvent( + eq(actionDownVolumeUpKeyEvent), + eq(AudioManager.USE_DEFAULT_STREAM_TYPE), + eq(true) + ) + } + + @Test + fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_menuActionUp_interactiveShadeLocked_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) + + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + @Test + fun dispatchKeyEventPreIme_back_keyguard_onBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true) + + whenever(backActionInteractor.onBackRequested()).thenReturn(false) + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor).onBackRequested() + clearInvocations(backActionInteractor) + + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isTrue() + verify(backActionInteractor).onBackRequested() + } + + @Test + fun dispatchKeyEventPreIme_back_keyguard_SBKVMdoesNotHandle_neverOnBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(false) + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor, never()).onBackRequested() + } + + @Test + fun dispatchKeyEventPreIme_back_shade_neverOnBackRequested() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true) + whenever(backActionInteractor.onBackRequested()).thenReturn(true) + + assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse() + verify(backActionInteractor, never()).onBackRequested() + } + + @Test + fun interceptMediaKey_keyguard_SBKVMdoesNotHandle_doesNotHandleMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(false) + + assertThat(underTest.interceptMediaKey(keyEvent)).isFalse() + verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent)) + } + + @Test + fun interceptMediaKey_keyguard_handleMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(true) + + assertThat(underTest.interceptMediaKey(keyEvent)).isTrue() + verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent)) + } + + @Test + fun interceptMediaKey_shade_doesNotHandleMediaKey() { + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + + assertThat( + underTest.interceptMediaKey( + KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP) + ) + ) + .isFalse() + verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 16cc924b5754..713c6027d642 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -147,16 +147,4 @@ class SceneInteractorTest : SysuiTestCase() { underTest.setVisible(true, "reason") assertThat(isVisible).isTrue() } - - @Test - fun remoteUserInput() = - testScope.runTest { - val remoteUserInput by collectLastValue(underTest.remoteUserInput) - assertThat(remoteUserInput).isNull() - - for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) { - underTest.onRemoteUserInput(input) - assertThat(remoteUserInput).isEqualTo(input) - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index da6c42694666..88abb642f7c1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -18,19 +18,14 @@ package com.android.systemui.scene.ui.viewmodel -import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils -import com.android.systemui.scene.shared.model.RemoteUserInput -import com.android.systemui.scene.shared.model.RemoteUserInputAction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -73,35 +68,4 @@ class SceneContainerViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) } - - @Test - fun onRemoteUserInput() = runTest { - val remoteUserInput by collectLastValue(underTest.remoteUserInput) - assertThat(remoteUserInput).isNull() - - val inputs = - SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) } - - inputs.forEachIndexed { index, input -> - underTest.onRemoteUserInput(input) - assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index]) - } - } - - private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent { - return MotionEvent.obtain( - currentTime, - currentTime, - when (input.action) { - RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN - RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE - RemoteUserInputAction.UP -> MotionEvent.ACTION_UP - RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL - RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE - }, - input.x, - input.y, - 0 - ) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt index 2d3ee0e5cff9..ca4486b533ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -20,12 +20,13 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri -import androidx.test.ext.truth.content.IntentSubject.assertThat +import androidx.test.ext.truth.content.IntentSubject.assertThat as assertThatIntent import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Test import org.mockito.Mockito.`when` as whenever @@ -39,23 +40,23 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createShare(uri) - assertThat(output).hasAction(Intent.ACTION_CHOOSER) - assertThat(output) + assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) + assertThatIntent(output) .hasFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION ) - assertThat(output).extras().parcelable<Intent>(Intent.EXTRA_INTENT).isNotNull() + assertThatIntent(output).extras().parcelable<Intent>(Intent.EXTRA_INTENT).isNotNull() val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - assertThat(wrappedIntent).hasAction(Intent.ACTION_SEND) - assertThat(wrappedIntent).hasData(uri) - assertThat(wrappedIntent).hasType("image/png") - assertThat(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_SUBJECT) - assertThat(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_TEXT) - assertThat(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) + assertThatIntent(wrappedIntent).hasAction(Intent.ACTION_SEND) + assertThatIntent(wrappedIntent).hasData(uri) + assertThatIntent(wrappedIntent).hasType("image/png") + assertThatIntent(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_SUBJECT) + assertThatIntent(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_TEXT) + assertThatIntent(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) } @Test @@ -64,7 +65,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createShare(uri) - assertThat(output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) + assertThatIntent(output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) .hasData(Uri.parse("content://fake")) } @@ -75,8 +76,8 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createShareWithSubject(uri, subject) - assertThat(output).hasAction(Intent.ACTION_CHOOSER) - assertThat(output) + assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) + assertThatIntent(output) .hasFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or @@ -84,12 +85,12 @@ class ActionIntentCreatorTest : SysuiTestCase() { ) val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - assertThat(wrappedIntent).hasAction(Intent.ACTION_SEND) - assertThat(wrappedIntent).hasData(uri) - assertThat(wrappedIntent).hasType("image/png") - assertThat(wrappedIntent).extras().string(Intent.EXTRA_SUBJECT).isEqualTo(subject) - assertThat(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_TEXT) - assertThat(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) + assertThatIntent(wrappedIntent).hasAction(Intent.ACTION_SEND) + assertThatIntent(wrappedIntent).hasData(uri) + assertThatIntent(wrappedIntent).hasType("image/png") + assertThatIntent(wrappedIntent).extras().string(Intent.EXTRA_SUBJECT).isEqualTo(subject) + assertThatIntent(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_TEXT) + assertThatIntent(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) } @Test @@ -99,8 +100,8 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createShareWithText(uri, extraText) - assertThat(output).hasAction(Intent.ACTION_CHOOSER) - assertThat(output) + assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) + assertThatIntent(output) .hasFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or @@ -108,12 +109,12 @@ class ActionIntentCreatorTest : SysuiTestCase() { ) val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - assertThat(wrappedIntent).hasAction(Intent.ACTION_SEND) - assertThat(wrappedIntent).hasData(uri) - assertThat(wrappedIntent).hasType("image/png") - assertThat(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_SUBJECT) - assertThat(wrappedIntent).extras().string(Intent.EXTRA_TEXT).isEqualTo(extraText) - assertThat(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) + assertThatIntent(wrappedIntent).hasAction(Intent.ACTION_SEND) + assertThatIntent(wrappedIntent).hasData(uri) + assertThatIntent(wrappedIntent).hasType("image/png") + assertThatIntent(wrappedIntent).extras().doesNotContainKey(Intent.EXTRA_SUBJECT) + assertThatIntent(wrappedIntent).extras().string(Intent.EXTRA_TEXT).isEqualTo(extraText) + assertThatIntent(wrappedIntent).extras().parcelable<Uri>(Intent.EXTRA_STREAM).isEqualTo(uri) } @Test @@ -125,11 +126,12 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createEdit(uri, context) - assertThat(output).hasAction(Intent.ACTION_EDIT) - assertThat(output).hasData(uri) - assertThat(output).hasType("image/png") + assertThatIntent(output).hasAction(Intent.ACTION_EDIT) + assertThatIntent(output).hasData(uri) + assertThatIntent(output).hasType("image/png") assertWithMessage("getComponent()").that(output.component).isNull() - assertThat(output) + assertThat(output.getStringExtra("edit_source")).isEqualTo("screenshot") + assertThatIntent(output) .hasFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or @@ -146,7 +148,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createEdit(uri, context) - assertThat(output).hasData(Uri.parse("content://fake")) + assertThatIntent(output).hasData(Uri.parse("content://fake")) } @Test @@ -160,6 +162,6 @@ class ActionIntentCreatorTest : SysuiTestCase() { val output = ActionIntentCreator.createEdit(uri, context) - assertThat(output).hasComponent(component) + assertThatIntent(output).hasComponent(component) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 981e44bea846..5cad9f821c96 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -73,6 +73,7 @@ import com.android.keyguard.KeyguardStatusView; import com.android.keyguard.KeyguardStatusViewController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.LockIconViewController; +import com.android.keyguard.TestScopeProvider; import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent; import com.android.keyguard.dagger.KeyguardStatusBarViewComponent; import com.android.keyguard.dagger.KeyguardStatusViewComponent; @@ -119,6 +120,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.qs.QSFragment; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shade.transition.ShadeTransitionController; @@ -134,6 +136,7 @@ import com.android.systemui.statusbar.QsFrameTranslateController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; @@ -164,14 +167,17 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; import com.android.systemui.statusbar.phone.TapAgainViewController; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; +import com.android.systemui.user.domain.interactor.UserInteractor; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; @@ -358,6 +364,16 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFakeKeyguardRepository = keyguardInteractorDeps.getRepository(); mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(mFakeKeyguardRepository); mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor(); + mShadeRepository = new FakeShadeRepository(); + mShadeInteractor = new ShadeInteractor( + TestScopeProvider.getTestScope(), + mShadeRepository, + new FakeDisableFlagsRepository(), + new FakeKeyguardRepository(), + new FakeUserSetupRepository(), + mock(DeviceProvisionedController.class), + mock(UserInteractor.class) + ); SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager, @@ -584,19 +600,33 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mMainHandler, mLayoutInflater, mFeatureFlags, - coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController, - mFalsingManager, new FalsingCollectorFake(), + coordinator, + expansionHandler, + mDynamicPrivacyController, + mKeyguardBypassController, + mFalsingManager, + new FalsingCollectorFake(), mKeyguardStateController, mStatusBarStateController, mStatusBarWindowStateController, mNotificationShadeWindowController, - mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper, - mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor, + mDozeLog, + mDozeParameters, + mCommandQueue, + mVibratorHelper, + mLatencyTracker, + mPowerManager, + mAccessibilityManager, + 0, + mUpdateMonitor, mMetricsLogger, mShadeLog, + mShadeInteractor, mConfigurationController, - () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, - mConversationNotificationManager, mMediaHierarchyManager, + () -> flingAnimationUtilsBuilder, + mStatusBarTouchableRegionManager, + mConversationNotificationManager, + mMediaHierarchyManager, mStatusBarKeyguardViewManager, mGutsManager, mNotificationsQSContainerController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index a2c291281fdf..35a54d14d1bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -49,6 +49,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.qs.QSFragment; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shade.transition.ShadeTransitionController; @@ -170,6 +171,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractor( mTestScope.getBackgroundScope(), + new FakeShadeRepository(), mDisableFlagsRepository, mKeyguardRepository, new FakeUserSetupRepository(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt index cdcd1a257671..9e6c12f65d43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt @@ -67,6 +67,7 @@ class ShadeInteractorTest : SysuiTestCase() { private val featureFlags = FakeFeatureFlags() private val userSetupRepository = FakeUserSetupRepository() private val userRepository = FakeUserRepository() + private val shadeRepository = FakeShadeRepository() private val disableFlagsRepository = FakeDisableFlagsRepository() private val keyguardRepository = FakeKeyguardRepository() @@ -138,6 +139,7 @@ class ShadeInteractorTest : SysuiTestCase() { underTest = ShadeInteractor( testScope.backgroundScope, + shadeRepository, disableFlagsRepository, keyguardRepository, userSetupRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 4a2518ae6f7d..ec4367c3f794 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -102,8 +102,10 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback private val disableFlagsRepository = FakeDisableFlagsRepository() private val keyguardRepository = FakeKeyguardRepository() + private val shadeRepository = FakeShadeRepository() private val shadeInteractor = ShadeInteractor( testScope.backgroundScope, + shadeRepository, disableFlagsRepository, keyguardRepository, userSetupRepository = FakeUserSetupRepository(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index 4a94dc819a9e..38a8f414b0fb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -21,11 +21,21 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener +import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.withArgCaptor import org.junit.Assert import org.junit.Before import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever @SmallTest @@ -36,13 +46,43 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val groupMembershipManager: GroupMembershipManager = mock() private val featureFlags = FakeFeatureFlags() - private val entry1 = NotificationEntryBuilder().build() - private val entry2 = NotificationEntryBuilder().build() + private val pipeline: NotifPipeline = mock() + private lateinit var beforeRenderListListener: OnBeforeRenderListListener + + private val summary1 = notificationEntry("foo", 1) + private val summary2 = notificationEntry("bar", 1) + private val entries = + listOf<ListEntry>( + GroupEntryBuilder() + .setSummary(summary1) + .setChildren( + listOf( + notificationEntry("foo", 2), + notificationEntry("foo", 3), + notificationEntry("foo", 4) + ) + ) + .build(), + GroupEntryBuilder() + .setSummary(summary2) + .setChildren( + listOf( + notificationEntry("bar", 2), + notificationEntry("bar", 3), + notificationEntry("bar", 4) + ) + ) + .build(), + notificationEntry("baz", 1) + ) + + private fun notificationEntry(pkg: String, id: Int) = + NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() } @Before fun setUp() { - whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1) - whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2) + whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) + whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags) } @@ -54,15 +94,15 @@ class GroupExpansionManagerTest : SysuiTestCase() { var listenerCalledCount = 0 gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - gem.setGroupExpanded(entry1, false) + gem.setGroupExpanded(summary1, false) Assert.assertEquals(0, listenerCalledCount) - gem.setGroupExpanded(entry1, true) + gem.setGroupExpanded(summary1, true) Assert.assertEquals(1, listenerCalledCount) - gem.setGroupExpanded(entry2, true) + gem.setGroupExpanded(summary2, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(entry1, true) + gem.setGroupExpanded(summary1, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(entry2, false) + gem.setGroupExpanded(summary2, false) Assert.assertEquals(3, listenerCalledCount) } @@ -73,15 +113,39 @@ class GroupExpansionManagerTest : SysuiTestCase() { var listenerCalledCount = 0 gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - gem.setGroupExpanded(entry1, false) + gem.setGroupExpanded(summary1, false) Assert.assertEquals(1, listenerCalledCount) - gem.setGroupExpanded(entry1, true) + gem.setGroupExpanded(summary1, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(entry2, true) + gem.setGroupExpanded(summary2, true) Assert.assertEquals(3, listenerCalledCount) - gem.setGroupExpanded(entry1, true) + gem.setGroupExpanded(summary1, true) Assert.assertEquals(4, listenerCalledCount) - gem.setGroupExpanded(entry2, false) + gem.setGroupExpanded(summary2, false) Assert.assertEquals(5, listenerCalledCount) } + + @Test + fun testSyncWithPipeline() { + featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + gem.attach(pipeline) + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + val listener: OnGroupExpansionChangeListener = mock() + gem.registerGroupExpansionChangeListener(listener) + + beforeRenderListListener.onBeforeRenderList(entries) + verify(listener, never()).onGroupExpansionChange(any(), any()) + + // Expand one of the groups. + gem.setGroupExpanded(summary1, true) + verify(listener).onGroupExpansionChange(summary1.row, true) + + // Empty the pipeline list and verify that the group is no longer expanded. + beforeRenderListListener.onBeforeRenderList(emptyList()) + verify(listener).onGroupExpansionChange(summary1.row, false) + verifyNoMoreInteractions(listener) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 987861d3f133..77c9b8b48296 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes +import android.content.pm.PackageManager import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress @@ -21,6 +22,7 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import org.junit.Assume import org.junit.Before import org.junit.Rule import org.junit.Test @@ -66,6 +68,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Before fun setUp() { + Assume.assumeFalse(isTv()) + whenever(notificationShelf.viewState).thenReturn(ExpandableViewState()) whenever(notificationRow.viewState).thenReturn(ExpandableViewState()) ambientState.isSmallScreen = true @@ -73,6 +77,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { hostView.addView(notificationRow) } + private fun isTv(): Boolean { + return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + } + @Test fun resetViewStates_defaultHun_yTranslationIsInset() { whenever(notificationRow.isPinned).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index 8545b894ad41..3ad3c15f158a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -29,8 +31,10 @@ import android.app.ActivityManager; import android.app.StatusBarManager; import android.os.PowerManager; import android.os.UserHandle; +import android.os.VibrationEffect; import android.os.Vibrator; import android.testing.AndroidTestingRunner; +import android.view.HapticFeedbackConstants; import android.view.WindowInsets; import androidx.test.filters.SmallTest; @@ -42,6 +46,7 @@ import com.android.internal.view.AppearanceRegion; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; @@ -98,6 +103,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @Mock private UserTracker mUserTracker; @Mock private QSHost mQSHost; @Mock private ActivityStarter mActivityStarter; + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); CentralSurfacesCommandQueueCallbacks mSbcqCallbacks; @@ -134,7 +140,8 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { mCameraLauncherLazy, mUserTracker, mQSHost, - mActivityStarter); + mActivityStarter, + mFeatureFlags); when(mUserTracker.getUserHandle()).thenReturn( UserHandle.of(ActivityManager.getCurrentUser())); @@ -241,4 +248,24 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { verifyZeroInteractions(mSystemBarAttributesListener); } + + @Test + public void vibrateOnNavigationKeyDown_oneWayHapticsDisabled_usesVibrate() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); + + mSbcqCallbacks.vibrateOnNavigationKeyDown(); + + verify(mVibratorHelper).vibrate(VibrationEffect.EFFECT_TICK); + } + + @Test + public void vibrateOnNavigationKeyDown_oneWayHapticsEnabled_usesPerformHapticFeedback() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + + mSbcqCallbacks.vibrateOnNavigationKeyDown(); + + verify(mShadeViewController).performHapticFeedback( + HapticFeedbackConstants.GESTURE_START + ); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 2e92bb948c60..0b31523e9b98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -27,7 +27,7 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.ShadeControllerImpl import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController @@ -77,7 +77,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var shadeControllerImpl: ShadeControllerImpl @Mock - private lateinit var sceneInteractor: Provider<SceneInteractor> + private lateinit var windowRootView: Provider<WindowRootView> @Mock private lateinit var shadeLogger: ShadeLogger @Mock @@ -203,7 +203,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { centralSurfacesImpl, shadeControllerImpl, shadeViewController, - sceneInteractor, + windowRootView, shadeLogger, viewUtil, configurationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 823155b0d7e6..b8f2cab3019e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -81,7 +81,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; -import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -136,8 +136,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private StatusBarWindowStateController mStatusBarWindowStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @ClassRule - public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index ef39ff8ed521..79feb417a062 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -82,7 +82,7 @@ import com.android.systemui.statusbar.phone.LightBarController; import org.junit.After; import org.junit.Before; -import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -111,8 +111,8 @@ public class RemoteInputViewTest extends SysuiTestCase { private BlockingQueueIntentReceiver mReceiver; private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); - @ClassRule - public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); @Before public void setUp() throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt index b698e70eb379..b78e83990411 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -105,6 +106,7 @@ class VariableDateViewControllerTest : SysuiTestCase() { systemClock, broadcastDispatcher, shadeExpansionStateManager, + mock(), testableHandler, view ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 462fd0a2410b..69d7586e6ab4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -104,8 +104,6 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private DumpManager mDumpManager; - @Mock - private Handler mHandler; @Before @@ -130,7 +128,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, mPackageManager, mWakefullnessLifcycle, mKeyguardManager, - mActivityManager, mUserTracker, mDumpManager, mHandler, mCallback); + mActivityManager, mUserTracker, mDumpManager, mCallback); mVolumeController.setEnableDialogs(true, true); } @@ -245,12 +243,11 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { ActivityManager activityManager, UserTracker userTracker, DumpManager dumpManager, - Handler mainHandler, C callback) { super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, notificationManager, optionalVibrator, iAudioService, accessibilityManager, packageManager, wakefulnessLifecycle, keyguardManager, - activityManager, userTracker, dumpManager, mainHandler); + activityManager, userTracker, dumpManager); mCallbacks = callback; ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index fa18e575220c..5f0011bc809a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -31,6 +31,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -38,7 +39,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.AnimatorTestRule; import android.app.KeyguardManager; import android.content.res.Configuration; import android.media.AudioManager; @@ -85,14 +85,14 @@ import java.util.function.Predicate; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class VolumeDialogImplTest extends SysuiTestCase { - private static final AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule(); - VolumeDialogImpl mDialog; View mActiveRinger; View mDrawerContainer; View mDrawerVibrate; View mDrawerMute; View mDrawerNormal; + CaptionsToggleImageButton mODICaptionsIcon; + private TestableLooper mTestableLooper; private ConfigurationController mConfigurationController; private int mOriginalOrientation; @@ -177,9 +177,14 @@ public class VolumeDialogImplTest extends SysuiTestCase { mActiveRinger = mDialog.getDialogView().findViewById( R.id.volume_new_ringer_active_icon_container); mDrawerContainer = mDialog.getDialogView().findViewById(R.id.volume_drawer_container); - mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate); - mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute); - mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal); + + // Drawer is not always available, e.g. on TVs + if (mDrawerContainer != null) { + mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate); + mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute); + mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal); + } + mODICaptionsIcon = mDialog.getDialogView().findViewById(R.id.odi_captions_icon); Prefs.putInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, @@ -188,6 +193,10 @@ public class VolumeDialogImplTest extends SysuiTestCase { Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false); } + private void assumeHasDrawer() { + assumeNotNull("Layout does not contain drawer", mDrawerContainer); + } + private State createShellState() { State state = new VolumeDialogController.State(); for (int i = AudioManager.STREAM_VOICE_CALL; i <= AudioManager.STREAM_ACCESSIBILITY; i++) { @@ -359,6 +368,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectVibrateFromDrawer() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; @@ -374,6 +385,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectVibrateFromDrawer_OnewayAPI_On() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL; @@ -389,6 +402,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectMuteFromDrawer() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; @@ -404,6 +419,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectMuteFromDrawer_OnewayAPI_On() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL; @@ -419,6 +436,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectNormalFromDrawer() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; @@ -434,6 +453,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectNormalFromDrawer_OnewayAPI_On() { + assumeHasDrawer(); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; @@ -688,6 +709,28 @@ public class VolumeDialogImplTest extends SysuiTestCase { assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.CLOSE); } + @Test + public void testOnCaptionEnabledStateChanged_checkBeforeSwitchTrue_setCaptionsEnabledState() { + ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture = + ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class); + verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any()); + VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue(); + + callbacks.onCaptionEnabledStateChanged(true, true); + verify(mVolumeDialogController).setCaptionsEnabledState(eq(false)); + } + + @Test + public void testOnCaptionEnabledStateChanged_checkBeforeSwitchFalse_getCaptionsEnabledTrue() { + ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture = + ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class); + verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any()); + VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue(); + + callbacks.onCaptionEnabledStateChanged(true, false); + assertTrue(mODICaptionsIcon.getCaptionsEnabled()); + } + /** * The content description should include ringer state, and the correct one. */ @@ -727,7 +770,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { public void teardown() { cleanUp(mDialog); setOrientation(mOriginalOrientation); - sAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration); mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration); mTestableLooper.processAllMessages(); reset(mPostureController); diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java index 19c68e86e5dc..41dbc147dfc5 100644 --- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java +++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java @@ -49,7 +49,7 @@ import java.util.function.Consumer; * public class SampleAnimatorTest { * * {@literal @}Rule - * public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule(); + * public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); * * {@literal @}UiThreadTest * {@literal @}Test @@ -58,7 +58,7 @@ import java.util.function.Consumer; * animator.setDuration(1000L); * assertThat(animator.getAnimatedValue(), is(0)); * animator.start(); - * sAnimatorTestRule.advanceTimeBy(500L); + * mAnimatorTestRule.advanceTimeBy(500L); * assertThat(animator.getAnimatedValue(), is(500)); * } * } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index 8c98aea6a990..e91e9559fa1e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -22,32 +22,24 @@ import com.android.systemui.keyguard.shared.model.AuthenticationFlags import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map class FakeBiometricSettingsRepository : BiometricSettingsRepository { + private val _isFingerprintEnrolledAndEnabled = MutableStateFlow(false) + override val isFingerprintEnrolledAndEnabled: StateFlow<Boolean> + get() = _isFingerprintEnrolledAndEnabled - private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false) - override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow() + private val _isFingerprintAuthCurrentlyAllowed = MutableStateFlow(false) + override val isFingerprintAuthCurrentlyAllowed: StateFlow<Boolean> + get() = _isFingerprintAuthCurrentlyAllowed - private val _isFaceEnrolled = MutableStateFlow(false) - override val isFaceEnrolled: Flow<Boolean> - get() = _isFaceEnrolled + private val _isFaceAuthEnrolledAndEnabled = MutableStateFlow(false) + override val isFaceAuthEnrolledAndEnabled: Flow<Boolean> + get() = _isFaceAuthEnrolledAndEnabled - private val _isFaceAuthEnabled = MutableStateFlow(false) - override val isFaceAuthenticationEnabled: Flow<Boolean> - get() = _isFaceAuthEnabled - - private val _isStrongBiometricAllowed = MutableStateFlow(false) - override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow() - - private val _isNonStrongBiometricAllowed = MutableStateFlow(false) - override val isNonStrongBiometricAllowed: StateFlow<Boolean> - get() = _isNonStrongBiometricAllowed - - private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false) - override val isFingerprintEnabledByDevicePolicy = - _isFingerprintEnabledByDevicePolicy.asStateFlow() + private val _isFaceAuthCurrentlyAllowed = MutableStateFlow(false) + override val isFaceAuthCurrentlyAllowed: Flow<Boolean> + get() = _isFaceAuthCurrentlyAllowed private val _isFaceAuthSupportedInCurrentPosture = MutableStateFlow(false) override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> @@ -59,34 +51,33 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0)) override val authenticationFlags: Flow<AuthenticationFlags> get() = _authFlags - fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { - _isFingerprintEnrolled.value = isFingerprintEnrolled + + fun setAuthenticationFlags(value: AuthenticationFlags) { + _authFlags.value = value } - fun setStrongBiometricAllowed(isStrongBiometricAllowed: Boolean) { - _isStrongBiometricAllowed.value = isStrongBiometricAllowed + fun setIsFingerprintAuthEnrolledAndEnabled(value: Boolean) { + _isFingerprintEnrolledAndEnabled.value = value + _isFingerprintAuthCurrentlyAllowed.value = _isFingerprintAuthCurrentlyAllowed.value && value } - fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) { - _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy + fun setIsFingerprintAuthCurrentlyAllowed(value: Boolean) { + _isFingerprintAuthCurrentlyAllowed.value = value } - fun setAuthenticationFlags(value: AuthenticationFlags) { - _authFlags.value = value + fun setIsFaceAuthEnrolledAndEnabled(value: Boolean) { + _isFaceAuthEnrolledAndEnabled.value = value + _isFaceAuthCurrentlyAllowed.value = _isFaceAuthCurrentlyAllowed.value && value } - fun setFaceEnrolled(isFaceEnrolled: Boolean) { - _isFaceEnrolled.value = isFaceEnrolled + fun setIsFaceAuthCurrentlyAllowed(value: Boolean) { + _isFaceAuthCurrentlyAllowed.value = value } fun setIsFaceAuthSupportedInCurrentPosture(value: Boolean) { _isFaceAuthSupportedInCurrentPosture.value = value } - fun setIsFaceAuthEnabled(enabled: Boolean) { - _isFaceAuthEnabled.value = enabled - } - fun setIsUserInLockdown(value: Boolean) { if (value) { setAuthenticationFlags( @@ -105,12 +96,4 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { ) } } - - fun setIsNonStrongBiometricAllowed(value: Boolean) { - _isNonStrongBiometricAllowed.value = value - } - - fun setIsStrongBiometricAllowed(value: Boolean) { - _isStrongBiometricAllowed.value = value - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index dd45331df4b3..f0e1111c6e12 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -39,8 +39,6 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.RemoteUserInput -import com.android.systemui.scene.shared.model.RemoteUserInputAction import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.user.data.repository.FakeUserRepository @@ -196,15 +194,6 @@ class SceneTestUtils( } companion object { - val REMOTE_INPUT_DOWN_GESTURE = - listOf( - RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN), - RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE), - RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE), - RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE), - RemoteUserInput(10f, 40f, RemoteUserInputAction.UP), - ) - fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel { return when (this) { DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt index 492e5421b010..35a3fd01675d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt @@ -30,6 +30,9 @@ class FakeShadeRepository : ShadeRepository { private val _qsExpansion = MutableStateFlow(0f) override val qsExpansion = _qsExpansion + private val _expansion = MutableStateFlow(0f) + override val expansion = _expansion + private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f) override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress @@ -41,6 +44,10 @@ class FakeShadeRepository : ShadeRepository { _qsExpansion.value = qsExpansion } + override fun setExpansion(expansion: Float) { + _expansion.value = expansion + } + override fun setUdfpsTransitionToFullShadeProgress(progress: Float) { _udfpsTransitionToFullShadeProgress.value = progress } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 830d55ad866b..7fb9580b28ab 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1289,6 +1289,16 @@ class StorageManagerService extends IStorageManager.Stub return mVold.supportsBlockCheckpoint(); } + private void prepareUserStorageForMoveInternal(String fromVolumeUuid, String toVolumeUuid, + List<UserInfo> users) throws Exception { + + final int flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE; + for (UserInfo user : users) { + prepareUserStorageInternal(fromVolumeUuid, user.id, user.serialNumber, flags); + prepareUserStorageInternal(toVolumeUuid, user.id, user.serialNumber, flags); + } + } + @Override public void onAwakeStateChanged(boolean isAwake) { // Ignored @@ -2912,6 +2922,7 @@ class StorageManagerService extends IStorageManager.Stub final VolumeInfo from; final VolumeInfo to; + final List<UserInfo> users; synchronized (mLock) { if (Objects.equals(mPrimaryStorageUuid, volumeUuid)) { @@ -2925,7 +2936,7 @@ class StorageManagerService extends IStorageManager.Stub mMoveTargetUuid = volumeUuid; // We need all the users unlocked to move their primary storage - final List<UserInfo> users = mContext.getSystemService(UserManager.class).getUsers(); + users = mContext.getSystemService(UserManager.class).getUsers(); for (UserInfo user : users) { if (StorageManager.isFileEncrypted() && !isUserKeyUnlocked(user.id)) { Slog.w(TAG, "Failing move due to locked user " + user.id); @@ -2961,6 +2972,19 @@ class StorageManagerService extends IStorageManager.Stub } } + // Prepare the storage before move, this is required to unlock adoptable storage (as the + // keys are tied to prepare user data step) & also is required for the destination files to + // end up with the correct SELinux labels and encryption policies for directories + try { + prepareUserStorageForMoveInternal(mPrimaryStorageUuid, volumeUuid, users); + } catch (Exception e) { + Slog.w(TAG, "Failing move due to failure on prepare user data", e); + synchronized (mLock) { + onMoveStatusLocked(PackageManager.MOVE_FAILED_INTERNAL_ERROR); + } + return; + } + try { mVold.moveStorage(from.id, to.id, new IVoldTaskListener.Stub() { @Override @@ -4904,5 +4928,16 @@ class StorageManagerService extends IStorageManager.Stub mCloudProviderChangeListeners.add(listener); mHandler.obtainMessage(H_CLOUD_MEDIA_PROVIDER_CHANGED, listener).sendToTarget(); } + + @Override + public void prepareUserStorageForMove(String fromVolumeUuid, String toVolumeUuid, + List<UserInfo> users) { + try { + prepareUserStorageForMoveInternal(fromVolumeUuid, toVolumeUuid, users); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java index 137a418e31ab..e109cc8011e7 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStats.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java @@ -22,6 +22,8 @@ package com.android.server.biometrics; */ public class AuthenticationStats { + private static final float FRR_NOT_ENOUGH_ATTEMPTS = -1.0f; + private final int mUserId; private int mTotalAttempts; private int mRejectedAttempts; @@ -70,7 +72,7 @@ public class AuthenticationStats { if (mTotalAttempts > 0) { return mRejectedAttempts / (float) mTotalAttempts; } else { - return -1.0f; + return FRR_NOT_ENOUGH_ATTEMPTS; } } @@ -87,4 +89,32 @@ public class AuthenticationStats { mTotalAttempts = 0; mRejectedAttempts = 0; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof AuthenticationStats)) { + return false; + } + + AuthenticationStats target = (AuthenticationStats) obj; + return this.getUserId() == target.getUserId() + && this.getTotalAttempts() + == target.getTotalAttempts() + && this.getRejectedAttempts() + == target.getRejectedAttempts() + && this.getEnrollmentNotifications() + == target.getEnrollmentNotifications() + && this.getModality() == target.getModality(); + } + + @Override + public int hashCode() { + return String.format("userId: %d, totalAttempts: %d, rejectedAttempts: %d, " + + "enrollmentNotifications: %d, modality: %d", mUserId, mTotalAttempts, + mRejectedAttempts, mEnrollmentNotifications, mModality).hashCode(); + } } diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java index c9cd8148c0f1..97e5c6fbd8c3 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java @@ -18,10 +18,18 @@ package com.android.server.biometrics; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.sensors.BiometricNotification; import java.util.HashMap; import java.util.Map; @@ -37,23 +45,60 @@ public class AuthenticationStatsCollector { // The minimum number of attempts that will calculate the FRR and trigger the notification. private static final int MINIMUM_ATTEMPTS = 500; + // Upload the data every 50 attempts (average number of daily authentications). + private static final int AUTHENTICATION_UPLOAD_INTERVAL = 50; // The maximum number of eligible biometric enrollment notification can be sent. private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2; + @NonNull private final Context mContext; + private final float mThreshold; private final int mModality; @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; - public AuthenticationStatsCollector(@NonNull Context context, int modality) { + // TODO(b/295582896): Find a way to make this NonNull + @Nullable private AuthenticationStatsPersister mAuthenticationStatsPersister; + @NonNull private BiometricNotification mBiometricNotification; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId != UserHandle.USER_NULL + && intent.getAction().equals(Intent.ACTION_USER_REMOVED)) { + onUserRemoved(userId); + } + } + }; + + public AuthenticationStatsCollector(@NonNull Context context, int modality, + @NonNull BiometricNotification biometricNotification) { + mContext = context; mThreshold = context.getResources() .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1); mUserAuthenticationStatsMap = new HashMap<>(); mModality = modality; + mBiometricNotification = biometricNotification; + + context.registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED)); + } + + private void initializeUserAuthenticationStatsMap() { + mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext); + for (AuthenticationStats stats : mAuthenticationStatsPersister.getAllFrrStats(mModality)) { + mUserAuthenticationStatsMap.put(stats.getUserId(), stats); + } } /** Update total authentication and rejected attempts. */ public void authenticate(int userId, boolean authenticated) { + // SharedPreference is not ready when starting system server, initialize + // mUserAuthenticationStatsMap in authentication to ensure SharedPreference + // is ready for application use. + if (mUserAuthenticationStatsMap.isEmpty()) { + initializeUserAuthenticationStatsMap(); + } // Check if this is a new user. if (!mUserAuthenticationStatsMap.containsKey(userId)) { mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); @@ -67,24 +112,65 @@ public class AuthenticationStatsCollector { sendNotificationIfNeeded(userId); } + /** Check if a notification should be sent after a calculation cycle. */ private void sendNotificationIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); - if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) { - // Send notification if FRR exceeds the threshold - if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS - && authenticationStats.getFrr() >= mThreshold) { - // TODO(wenhuiy): Send notifications. - } + if (authenticationStats.getTotalAttempts() < MINIMUM_ATTEMPTS) { + return; + } + // Don't send notification if FRR below the threshold. + if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS + || authenticationStats.getFrr() < mThreshold) { authenticationStats.resetData(); + return; + } + + authenticationStats.resetData(); + + final PackageManager packageManager = mContext.getPackageManager(); + + // Don't send notification to single-modality devices. + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) + || !packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { + return; + } + + final FaceManager faceManager = mContext.getSystemService(FaceManager.class); + final boolean hasEnrolledFace = faceManager.hasEnrolledTemplates(userId); + + final FingerprintManager fingerprintManager = mContext + .getSystemService(FingerprintManager.class); + final boolean hasEnrolledFingerprint = fingerprintManager.hasEnrolledTemplates(userId); + + // Don't send notification when both face and fingerprint are enrolled. + if (hasEnrolledFace && hasEnrolledFingerprint) { + return; + } + if (hasEnrolledFace && !hasEnrolledFingerprint) { + mBiometricNotification.sendFpEnrollNotification(mContext); + } else if (!hasEnrolledFace && hasEnrolledFingerprint) { + mBiometricNotification.sendFaceEnrollNotification(mContext); } } private void persistDataIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); - if (authenticationStats.getTotalAttempts() % 50 == 0) { - // TODO(wenhuiy): Persist data. + if (authenticationStats.getTotalAttempts() % AUTHENTICATION_UPLOAD_INTERVAL == 0) { + mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), + authenticationStats.getTotalAttempts(), + authenticationStats.getRejectedAttempts(), + authenticationStats.getEnrollmentNotifications(), + authenticationStats.getModality()); + } + } + + private void onUserRemoved(final int userId) { + if (mAuthenticationStatsPersister == null) { + initializeUserAuthenticationStatsMap(); } + mUserAuthenticationStatsMap.remove(userId); + mAuthenticationStatsPersister.removeFrrStats(userId); } /** diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java new file mode 100644 index 000000000000..21e93a8bc024 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.os.Environment; +import android.util.Slog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * Persists and retrieves stats for Biometric Authentication. + * Authentication stats include userId, total attempts, rejected attempts, + * and the number of sent enrollment notifications. + * Data are stored in SharedPreferences in a form of a set of JSON objects, + * where it's one element per user. + */ +public class AuthenticationStatsPersister { + + private static final String TAG = "AuthenticationStatsPersister"; + private static final String FILE_NAME = "authentication_stats"; + private static final String USER_ID = "user_id"; + private static final String FACE_ATTEMPTS = "face_attempts"; + private static final String FACE_REJECTIONS = "face_rejections"; + private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; + private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; + private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; + private static final String KEY = "frr_stats"; + + @NonNull private final SharedPreferences mSharedPreferences; + + AuthenticationStatsPersister(@NonNull Context context) { + // The package info in the context isn't initialized in the way it is for normal apps, + // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we + // build the path manually below using the same policy that appears in ContextImpl. + final File prefsFile = new File(Environment.getDataSystemDeDirectory(), FILE_NAME); + mSharedPreferences = context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE); + } + + /** + * Get all frr data from SharedPreference. + */ + public List<AuthenticationStats> getAllFrrStats(int modality) { + List<AuthenticationStats> authenticationStatsList = new ArrayList<>(); + for (String frrStats : readFrrStats()) { + try { + JSONObject frrStatsJson = new JSONObject(frrStats); + if (modality == BiometricsProtoEnums.MODALITY_FACE) { + authenticationStatsList.add(new AuthenticationStats( + getIntValue(frrStatsJson, USER_ID, -1 /* defaultValue */), + getIntValue(frrStatsJson, FACE_ATTEMPTS), + getIntValue(frrStatsJson, FACE_REJECTIONS), + getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), + modality)); + } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + authenticationStatsList.add(new AuthenticationStats( + getIntValue(frrStatsJson, USER_ID, -1 /* defaultValue */), + getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS), + getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS), + getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), + modality)); + } + } catch (JSONException e) { + Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s", + frrStats)); + } + } + return authenticationStatsList; + } + + /** + * Remove frr data for a specific user. + */ + public void removeFrrStats(int userId) { + try { + // Copy into a new HashSet to allow modification. + Set<String> frrStatsSet = new HashSet<>(readFrrStats()); + + // Remove the old authentication stat for the user if it exists. + for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) { + String frrStats = iterator.next(); + JSONObject frrStatJson = new JSONObject(frrStats); + if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) { + iterator.remove(); + break; + } + } + + mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply(); + } catch (JSONException ignored) { + } + } + + /** + * Persist frr data for a specific user. + */ + public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts, + int enrollmentNotifications, int modality) { + try { + // Copy into a new HashSet to allow modification. + Set<String> frrStatsSet = new HashSet<>(readFrrStats()); + + // Remove the old authentication stat for the user if it exists. + JSONObject frrStatJson = null; + for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) { + String frrStats = iterator.next(); + frrStatJson = new JSONObject(frrStats); + if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) { + iterator.remove(); + break; + } + } + + // If there's existing frr stats in the file, we want to update the stats for the given + // modality and keep the stats for other modalities. + if (frrStatJson != null) { + frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts, + enrollmentNotifications, modality)); + } else { + frrStatsSet.add(buildFrrStats(userId, totalAttempts, rejectedAttempts, + enrollmentNotifications, modality)); + } + + mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply(); + + } catch (JSONException e) { + Slog.e(TAG, "Unable to persist authentication stats"); + } + } + + private Set<String> readFrrStats() { + return mSharedPreferences.getStringSet(KEY, Set.of()); + } + + // Update frr stats for existing frrStats JSONObject and build the new string. + private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts, + int enrollmentNotifications, int modality) throws JSONException { + if (modality == BiometricsProtoEnums.MODALITY_FACE) { + return frrStats + .put(FACE_ATTEMPTS, totalAttempts) + .put(FACE_REJECTIONS, rejectedAttempts) + .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .toString(); + } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + return frrStats + .put(FINGERPRINT_ATTEMPTS, totalAttempts) + .put(FINGERPRINT_REJECTIONS, rejectedAttempts) + .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .toString(); + } else { + return frrStats.toString(); + } + } + + // Build string for new user and new authentication stats. + private String buildFrrStats(int userId, int totalAttempts, int rejectedAttempts, + int enrollmentNotifications, int modality) + throws JSONException { + if (modality == BiometricsProtoEnums.MODALITY_FACE) { + return new JSONObject() + .put(USER_ID, userId) + .put(FACE_ATTEMPTS, totalAttempts) + .put(FACE_REJECTIONS, rejectedAttempts) + .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .toString(); + } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + return new JSONObject() + .put(USER_ID, userId) + .put(FINGERPRINT_ATTEMPTS, totalAttempts) + .put(FINGERPRINT_REJECTIONS, rejectedAttempts) + .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) + .toString(); + } else { + return ""; + } + } + + private String getValue(JSONObject jsonObject, String key) throws JSONException { + return jsonObject.has(key) ? jsonObject.getString(key) : ""; + } + + private int getIntValue(JSONObject jsonObject, String key) throws JSONException { + return getIntValue(jsonObject, key, 0 /* defaultValue */); + } + + private int getIntValue(JSONObject jsonObject, String key, int defaultValue) + throws JSONException { + return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java new file mode 100644 index 000000000000..90e18604d945 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.annotation.NonNull; +import android.content.Context; + +/** + * Interface for biometrics to send notifications. + */ +public interface BiometricNotification { + + /** + * Sends a face enrollment notification. + */ + void sendFaceEnrollNotification(@NonNull Context context); + + /** + * Sends a fingerprint enrollment notification. + */ + void sendFpEnrollNotification(@NonNull Context context); +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java new file mode 100644 index 000000000000..7b420468f628 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.annotation.NonNull; +import android.content.Context; + +import com.android.server.biometrics.AuthenticationStatsCollector; + +/** + * Implementation to send biometric notifications for {@link AuthenticationStatsCollector}. + */ +public class BiometricNotificationImpl implements BiometricNotification { + + @Override + public void sendFaceEnrollNotification(@NonNull Context context) { + BiometricNotificationUtils.showFaceEnrollNotification(context); + } + + @Override + public void sendFpEnrollNotification(@NonNull Context context) { + BiometricNotificationUtils.showFingerprintEnrollNotification(context); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java index f516a4930a58..2ff695d7b85d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java @@ -35,9 +35,22 @@ import com.android.internal.R; public class BiometricNotificationUtils { private static final String TAG = "BiometricNotificationUtils"; - private static final String RE_ENROLL_NOTIFICATION_TAG = "FaceService"; - private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintService"; + private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll"; + private static final String FACE_ENROLL_NOTIFICATION_TAG = "FaceEnroll"; + private static final String FINGERPRINT_ENROLL_NOTIFICATION_TAG = "FingerprintEnroll"; + private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration"; private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock"; + private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS"; + private static final String FINGERPRINT_SETTINGS_ACTION = + "android.settings.FINGERPRINT_SETTINGS"; + private static final String FACE_ENROLL_ACTION = "android.settings.FACE_ENROLL"; + private static final String FINGERPRINT_ENROLL_ACTION = "android.settings.FINGERPRINT_ENROLL"; + private static final String SETTINGS_PACKAGE = "com.android.settings"; + private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel"; + private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel"; + private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel"; + private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL = + "FingerprintBadCalibrationNotificationChannel"; private static final int NOTIFICATION_ID = 1; private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000; private static long sLastAlertTime = 0; @@ -56,18 +69,67 @@ public class BiometricNotificationUtils { final String content = context.getString(R.string.face_recalibrate_notification_content); - final Intent intent = new Intent("android.settings.FACE_SETTINGS"); - intent.setPackage("com.android.settings"); + final Intent intent = new Intent(FACE_SETTINGS_ACTION); + intent.setPackage(SETTINGS_PACKAGE); intent.putExtra(KEY_RE_ENROLL_FACE, true); final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */, null /* options */, UserHandle.CURRENT); - final String channelName = "FaceEnrollNotificationChannel"; + showNotificationHelper(context, name, title, content, pendingIntent, FACE_RE_ENROLL_CHANNEL, + FACE_RE_ENROLL_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET); + } + + /** + * Shows a face enrollment notification. + */ + public static void showFaceEnrollNotification(@NonNull Context context) { + + final String name = + context.getString(R.string.device_unlock_notification_name); + final String title = + context.getString(R.string.alternative_unlock_setup_notification_title); + final String content = + context.getString(R.string.alternative_face_setup_notification_content); + + final Intent intent = new Intent(FACE_ENROLL_ACTION); + intent.setPackage(SETTINGS_PACKAGE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, + 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */, + null /* options */, UserHandle.CURRENT); + + showNotificationHelper(context, name, title, content, pendingIntent, FACE_ENROLL_CHANNEL, + FACE_ENROLL_NOTIFICATION_TAG, Notification.VISIBILITY_PUBLIC); + } + + /** + * Shows a fingerprint enrollment notification. + */ + public static void showFingerprintEnrollNotification(@NonNull Context context) { + + final String name = + context.getString(R.string.device_unlock_notification_name); + final String title = + context.getString(R.string.alternative_unlock_setup_notification_title); + final String content = + context.getString(R.string.alternative_fp_setup_notification_content); + + final Intent intent = new Intent(FINGERPRINT_ENROLL_ACTION); + intent.setPackage(SETTINGS_PACKAGE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, + 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */, + null /* options */, UserHandle.CURRENT); - showNotificationHelper(context, name, title, content, pendingIntent, channelName, - RE_ENROLL_NOTIFICATION_TAG); + showNotificationHelper(context, name, title, content, pendingIntent, + FINGERPRINT_ENROLL_CHANNEL, FINGERPRINT_ENROLL_NOTIFICATION_TAG, + Notification.VISIBILITY_PUBLIC); } /** @@ -93,22 +155,21 @@ public class BiometricNotificationUtils { final String content = context.getString(R.string.fingerprint_recalibrate_notification_content); - final Intent intent = new Intent("android.settings.FINGERPRINT_SETTINGS"); - intent.setPackage("com.android.settings"); + final Intent intent = new Intent(FINGERPRINT_SETTINGS_ACTION); + intent.setPackage(SETTINGS_PACKAGE); final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */, null /* options */, UserHandle.CURRENT); - final String channelName = "FingerprintBadCalibrationNotificationChannel"; - - showNotificationHelper(context, name, title, content, pendingIntent, channelName, - BAD_CALIBRATION_NOTIFICATION_TAG); + showNotificationHelper(context, name, title, content, pendingIntent, + FINGERPRINT_BAD_CALIBRATION_CHANNEL, BAD_CALIBRATION_NOTIFICATION_TAG, + Notification.VISIBILITY_SECRET); } private static void showNotificationHelper(Context context, String name, String title, String content, PendingIntent pendingIntent, String channelName, - String notificationTag) { + String notificationTag, int visibility) { final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); final NotificationChannel channel = new NotificationChannel(channelName, name, @@ -123,7 +184,7 @@ public class BiometricNotificationUtils { .setAutoCancel(true) .setCategory(Notification.CATEGORY_SYSTEM) .setContentIntent(pendingIntent) - .setVisibility(Notification.VISIBILITY_SECRET) + .setVisibility(visibility) .build(); notificationManager.createNotificationChannel(channel); @@ -134,10 +195,30 @@ public class BiometricNotificationUtils { /** * Cancels a face re-enrollment notification */ - public static void cancelReEnrollNotification(@NonNull Context context) { + public static void cancelFaceReEnrollNotification(@NonNull Context context) { + final NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + notificationManager.cancelAsUser(FACE_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID, + UserHandle.CURRENT); + } + + /** + * Cancels a face enrollment notification + */ + public static void cancelFaceEnrollNotification(@NonNull Context context) { + final NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + notificationManager.cancelAsUser(FACE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID, + UserHandle.CURRENT); + } + + /** + * Cancels a fingerprint enrollment notification + */ + public static void cancelFingerprintEnrollNotification(@NonNull Context context) { final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.cancelAsUser(RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID, + notificationManager.cancelAsUser(FINGERPRINT_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID, UserHandle.CURRENT); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java index 722c9afbeaf8..f55cf0549382 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java @@ -109,7 +109,8 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { public void start(@NonNull ClientMonitorCallback callback) { super.start(callback); - BiometricNotificationUtils.cancelReEnrollNotification(getContext()); + BiometricNotificationUtils.cancelFaceEnrollNotification(getContext()); + BiometricNotificationUtils.cancelFaceReEnrollNotification(getContext()); } @NonNull diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index a7d160c4fa60..28f0a4dadbd5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -56,6 +56,7 @@ import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricNotificationImpl; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; @@ -177,7 +178,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mDaemon = daemon; mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - BiometricsProtoEnums.MODALITY_FACE); + BiometricsProtoEnums.MODALITY_FACE, new BiometricNotificationImpl()); for (SensorProps prop : props) { final int sensorId = prop.commonProps.sensorId; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index 10991d5f9133..808626120c1e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -62,7 +62,7 @@ import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AcquisitionClient; import com.android.server.biometrics.sensors.AuthenticationConsumer; import com.android.server.biometrics.sensors.BaseClientMonitor; -import com.android.server.biometrics.sensors.BiometricNotificationUtils; +import com.android.server.biometrics.sensors.BiometricNotificationImpl; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; @@ -367,7 +367,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { }); mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - BiometricsProtoEnums.MODALITY_FACE); + BiometricsProtoEnums.MODALITY_FACE, new BiometricNotificationImpl()); try { ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); @@ -615,8 +615,6 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mHandler.post(() -> { scheduleUpdateActiveUserWithoutHandler(userId); - BiometricNotificationUtils.cancelReEnrollNotification(mContext); - final FaceEnrollClient client = new FaceEnrollClient(mContext, mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, opPackageName, id, FaceUtils.getLegacyInstance(mSensorId), disabledFeatures, diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java index 16d2f7a03c6d..27b9c79516af 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java @@ -33,6 +33,7 @@ import com.android.internal.R; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricNotificationUtils; import com.android.server.biometrics.sensors.BiometricUtils; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; @@ -71,6 +72,14 @@ public class FaceEnrollClient extends EnrollClient<IBiometricsFace> { .getIntArray(R.array.config_face_acquire_vendor_enroll_ignorelist); } + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + BiometricNotificationUtils.cancelFaceEnrollNotification(getContext()); + BiometricNotificationUtils.cancelFaceReEnrollNotification(getContext()); + } + @NonNull @Override protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index f9e08d69ef48..46ff6b4fab1a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -104,6 +104,13 @@ class FingerprintEnrollClient extends EnrollClient<AidlSession> implements Udfps } } + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + BiometricNotificationUtils.cancelFingerprintEnrollNotification(getContext()); + } + @NonNull @Override protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 2d062db12cdc..5f4b89439fd0 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -64,6 +64,7 @@ import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricNotificationImpl; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; @@ -184,7 +185,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mDaemon = daemon; mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - BiometricsProtoEnums.MODALITY_FINGERPRINT); + BiometricsProtoEnums.MODALITY_FINGERPRINT, new BiometricNotificationImpl()); final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 4b07dca75e9e..d0b71fcf2dbb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -66,6 +66,7 @@ import com.android.server.biometrics.sensors.AcquisitionClient; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.AuthenticationConsumer; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricNotificationImpl; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; @@ -354,7 +355,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider }); mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - BiometricsProtoEnums.MODALITY_FINGERPRINT); + BiometricsProtoEnums.MODALITY_FINGERPRINT, new BiometricNotificationImpl()); try { ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java index 6fee84a5e057..382e7e2121f4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java @@ -81,6 +81,13 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint } } + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + BiometricNotificationUtils.cancelFingerprintEnrollNotification(getContext()); + } + @NonNull @Override protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) { diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java index 4ad26c46d7ed..7ea576d1ed3a 100644 --- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java +++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java @@ -63,10 +63,15 @@ public class DisplayWhiteBalanceController implements // high errors. This default is introduced to provide a fixed display color // temperature when sensor readings become unreliable. private final float mLowLightAmbientColorTemperature; + // As above, but used when in strong mode (idle screen brightness mode). + private final float mLowLightAmbientColorTemperatureStrong; + // In high brightness conditions certain color temperatures can cause peak display // brightness to drop. This fixed color temperature can be used to compensate for // this effect. private final float mHighLightAmbientColorTemperature; + // As above, but used when in strong mode (idle screen brightness mode). + private final float mHighLightAmbientColorTemperatureStrong; private final boolean mLightModeAllowed; @@ -97,9 +102,11 @@ public class DisplayWhiteBalanceController implements // ambient color temperature to the defaults. A piecewise linear relationship // between low light brightness and low light bias. private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSpline; + private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSplineStrong; // A piecewise linear relationship between high light brightness and high light bias. private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSpline; + private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSplineStrong; private float mLatestAmbientColorTemperature; private float mLatestAmbientBrightness; @@ -134,17 +141,29 @@ public class DisplayWhiteBalanceController implements * @param lowLightAmbientBrightnesses * The ambient brightness used to map the ambient brightnesses to the biases used to * interpolate to lowLightAmbientColorTemperature. + * @param lowLightAmbientBrightnessesStrong + * The ambient brightness used to map the ambient brightnesses to the biases used to + * interpolate to lowLightAmbientColorTemperature. * @param lowLightAmbientBiases * The biases used to map the ambient brightnesses to the biases used to interpolate to * lowLightAmbientColorTemperature. + * @param lowLightAmbientBiasesStrong + * The biases used to map the ambient brightnesses to the biases used to interpolate to + * lowLightAmbientColorTemperature. * @param lowLightAmbientColorTemperature * The ambient color temperature to which we interpolate to based on the low light curve. * @param highLightAmbientBrightnesses * The ambient brightness used to map the ambient brightnesses to the biases used to * interpolate to highLightAmbientColorTemperature. + * @param highLightAmbientBrightnessesStrong + * The ambient brightness used to map the ambient brightnesses to the biases used to + * interpolate to highLightAmbientColorTemperature. * @param highLightAmbientBiases * The biases used to map the ambient brightnesses to the biases used to interpolate to * highLightAmbientColorTemperature. + * @param highLightAmbientBiasesStrong + * The biases used to map the ambient brightnesses to the biases used to interpolate to + * highLightAmbientColorTemperature. * @param highLightAmbientColorTemperature * The ambient color temperature to which we interpolate to based on the high light curve. * @param ambientColorTemperatures @@ -170,11 +189,17 @@ public class DisplayWhiteBalanceController implements @NonNull AmbientFilter colorTemperatureFilter, @NonNull DisplayWhiteBalanceThrottler throttler, float[] lowLightAmbientBrightnesses, + float[] lowLightAmbientBrightnessesStrong, float[] lowLightAmbientBiases, + float[] lowLightAmbientBiasesStrong, float lowLightAmbientColorTemperature, + float lowLightAmbientColorTemperatureStrong, float[] highLightAmbientBrightnesses, + float[] highLightAmbientBrightnessesStrong, float[] highLightAmbientBiases, + float[] highLightAmbientBiasesStrong, float highLightAmbientColorTemperature, + float highLightAmbientColorTemperatureStrong, float[] ambientColorTemperatures, float[] displayColorTemperatures, float[] strongAmbientColorTemperatures, @@ -188,7 +213,9 @@ public class DisplayWhiteBalanceController implements mColorTemperatureFilter = colorTemperatureFilter; mThrottler = throttler; mLowLightAmbientColorTemperature = lowLightAmbientColorTemperature; + mLowLightAmbientColorTemperatureStrong = lowLightAmbientColorTemperatureStrong; mHighLightAmbientColorTemperature = highLightAmbientColorTemperature; + mHighLightAmbientColorTemperatureStrong = highLightAmbientColorTemperatureStrong; mAmbientColorTemperature = -1.0f; mPendingAmbientColorTemperature = -1.0f; mLastAmbientColorTemperature = -1.0f; @@ -214,6 +241,23 @@ public class DisplayWhiteBalanceController implements } try { + mLowLightAmbientBrightnessToBiasSplineStrong = new Spline.LinearSpline( + lowLightAmbientBrightnessesStrong, lowLightAmbientBiasesStrong); + } catch (Exception e) { + Slog.e(TAG, "failed to create strong low light ambient brightness to bias spline.", e); + mLowLightAmbientBrightnessToBiasSplineStrong = null; + } + if (mLowLightAmbientBrightnessToBiasSplineStrong != null) { + if (mLowLightAmbientBrightnessToBiasSplineStrong.interpolate(0.0f) != 0.0f + || mLowLightAmbientBrightnessToBiasSplineStrong.interpolate( + Float.POSITIVE_INFINITY) != 1.0f) { + Slog.d(TAG, "invalid strong low light ambient brightness to bias spline, " + + "bias must begin at 0.0 and end at 1.0."); + mLowLightAmbientBrightnessToBiasSplineStrong = null; + } + } + + try { mHighLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline( highLightAmbientBrightnesses, highLightAmbientBiases); } catch (Exception e) { @@ -230,6 +274,23 @@ public class DisplayWhiteBalanceController implements } } + try { + mHighLightAmbientBrightnessToBiasSplineStrong = new Spline.LinearSpline( + highLightAmbientBrightnessesStrong, highLightAmbientBiasesStrong); + } catch (Exception e) { + Slog.e(TAG, "failed to create strong high light ambient brightness to bias spline.", e); + mHighLightAmbientBrightnessToBiasSplineStrong = null; + } + if (mHighLightAmbientBrightnessToBiasSplineStrong != null) { + if (mHighLightAmbientBrightnessToBiasSplineStrong.interpolate(0.0f) != 0.0f + || mHighLightAmbientBrightnessToBiasSplineStrong.interpolate( + Float.POSITIVE_INFINITY) != 1.0f) { + Slog.d(TAG, "invalid strong high light ambient brightness to bias spline, " + + "bias must begin at 0.0 and end at 1.0."); + mHighLightAmbientBrightnessToBiasSplineStrong = null; + } + } + if (mLowLightAmbientBrightnessToBiasSpline != null && mHighLightAmbientBrightnessToBiasSpline != null) { if (lowLightAmbientBrightnesses[lowLightAmbientBrightnesses.length - 1] > @@ -241,6 +302,18 @@ public class DisplayWhiteBalanceController implements } } + if (mLowLightAmbientBrightnessToBiasSplineStrong != null + && mHighLightAmbientBrightnessToBiasSplineStrong != null) { + if (lowLightAmbientBrightnessesStrong[lowLightAmbientBrightnessesStrong.length - 1] + > highLightAmbientBrightnessesStrong[0]) { + Slog.d(TAG, + "invalid strong low light and high light ambient brightness to bias " + + "spline combination, defined domains must not intersect."); + mLowLightAmbientBrightnessToBiasSplineStrong = null; + mHighLightAmbientBrightnessToBiasSplineStrong = null; + } + } + try { mAmbientToDisplayColorTemperatureSpline = new Spline.LinearSpline( ambientColorTemperatures, displayColorTemperatures); @@ -365,7 +438,11 @@ public class DisplayWhiteBalanceController implements mColorTemperatureFilter.dump(writer); mThrottler.dump(writer); writer.println(" mLowLightAmbientColorTemperature=" + mLowLightAmbientColorTemperature); + writer.println(" mLowLightAmbientColorTemperatureStrong=" + + mLowLightAmbientColorTemperatureStrong); writer.println(" mHighLightAmbientColorTemperature=" + mHighLightAmbientColorTemperature); + writer.println(" mHighLightAmbientColorTemperatureStrong=" + + mHighLightAmbientColorTemperatureStrong); writer.println(" mAmbientColorTemperature=" + mAmbientColorTemperature); writer.println(" mPendingAmbientColorTemperature=" + mPendingAmbientColorTemperature); writer.println(" mLastAmbientColorTemperature=" + mLastAmbientColorTemperature); @@ -377,8 +454,12 @@ public class DisplayWhiteBalanceController implements + mStrongAmbientToDisplayColorTemperatureSpline); writer.println(" mLowLightAmbientBrightnessToBiasSpline=" + mLowLightAmbientBrightnessToBiasSpline); + writer.println(" mLowLightAmbientBrightnessToBiasSplineStrong=" + + mLowLightAmbientBrightnessToBiasSplineStrong); writer.println(" mHighLightAmbientBrightnessToBiasSpline=" + mHighLightAmbientBrightnessToBiasSpline); + writer.println(" mHighLightAmbientBrightnessToBiasSplineStrong=" + + mHighLightAmbientBrightnessToBiasSplineStrong); } @Override // AmbientSensor.AmbientBrightnessSensor.Callbacks @@ -400,6 +481,17 @@ public class DisplayWhiteBalanceController implements */ public void updateAmbientColorTemperature() { final long time = System.currentTimeMillis(); + final float lowLightAmbientColorTemperature = mStrongModeEnabled + ? mLowLightAmbientColorTemperatureStrong : mLowLightAmbientColorTemperature; + final float highLightAmbientColorTemperature = mStrongModeEnabled + ? mHighLightAmbientColorTemperatureStrong : mHighLightAmbientColorTemperature; + final Spline.LinearSpline lowLightAmbientBrightnessToBiasSpline = mStrongModeEnabled + ? mLowLightAmbientBrightnessToBiasSplineStrong + : mLowLightAmbientBrightnessToBiasSpline; + final Spline.LinearSpline highLightAmbientBrightnessToBiasSpline = mStrongModeEnabled + ? mHighLightAmbientBrightnessToBiasSplineStrong + : mHighLightAmbientBrightnessToBiasSpline; + float ambientColorTemperature = mColorTemperatureFilter.getEstimate(time); mLatestAmbientColorTemperature = ambientColorTemperature; @@ -423,19 +515,19 @@ public class DisplayWhiteBalanceController implements mLatestAmbientBrightness = ambientBrightness; if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f - && mLowLightAmbientBrightnessToBiasSpline != null) { - float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness); + && lowLightAmbientBrightnessToBiasSpline != null) { + float bias = lowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness); ambientColorTemperature = bias * ambientColorTemperature + (1.0f - bias) - * mLowLightAmbientColorTemperature; + * lowLightAmbientColorTemperature; mLatestLowLightBias = bias; } if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f - && mHighLightAmbientBrightnessToBiasSpline != null) { - float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness); + && highLightAmbientBrightnessToBiasSpline != null) { + float bias = highLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness); ambientColorTemperature = (1.0f - bias) * ambientColorTemperature + bias - * mHighLightAmbientColorTemperature; + * highLightAmbientColorTemperature; mLatestHighLightBias = bias; } diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java index 62f813f2857a..39e6b3f288fb 100644 --- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java +++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java @@ -70,21 +70,39 @@ public class DisplayWhiteBalanceFactory { final float[] displayWhiteBalanceLowLightAmbientBrightnesses = getFloatArray(resources, com.android.internal.R.array .config_displayWhiteBalanceLowLightAmbientBrightnesses); + final float[] displayWhiteBalanceLowLightAmbientBrightnessesStrong = getFloatArray( + resources, com.android.internal.R.array + .config_displayWhiteBalanceLowLightAmbientBrightnessesStrong); final float[] displayWhiteBalanceLowLightAmbientBiases = getFloatArray(resources, com.android.internal.R.array .config_displayWhiteBalanceLowLightAmbientBiases); + final float[] displayWhiteBalanceLowLightAmbientBiasesStrong = getFloatArray(resources, + com.android.internal.R.array + .config_displayWhiteBalanceLowLightAmbientBiasesStrong); final float lowLightAmbientColorTemperature = getFloat(resources, com.android.internal.R.dimen .config_displayWhiteBalanceLowLightAmbientColorTemperature); + final float lowLightAmbientColorTemperatureStrong = getFloat(resources, + com.android.internal.R.dimen + .config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong); final float[] displayWhiteBalanceHighLightAmbientBrightnesses = getFloatArray(resources, com.android.internal.R.array .config_displayWhiteBalanceHighLightAmbientBrightnesses); + final float[] displayWhiteBalanceHighLightAmbientBrightnessesStrong = getFloatArray( + resources, com.android.internal.R.array + .config_displayWhiteBalanceHighLightAmbientBrightnessesStrong); final float[] displayWhiteBalanceHighLightAmbientBiases = getFloatArray(resources, com.android.internal.R.array .config_displayWhiteBalanceHighLightAmbientBiases); + final float[] displayWhiteBalanceHighLightAmbientBiasesStrong = getFloatArray(resources, + com.android.internal.R.array + .config_displayWhiteBalanceHighLightAmbientBiasesStrong); final float highLightAmbientColorTemperature = getFloat(resources, com.android.internal.R.dimen .config_displayWhiteBalanceHighLightAmbientColorTemperature); + final float highLightAmbientColorTemperatureStrong = getFloat(resources, + com.android.internal.R.dimen + .config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong); final float[] ambientColorTemperatures = getFloatArray(resources, com.android.internal.R.array.config_displayWhiteBalanceAmbientColorTemperatures); final float[] displayColorTemperatures = getFloatArray(resources, @@ -100,9 +118,15 @@ public class DisplayWhiteBalanceFactory { final DisplayWhiteBalanceController controller = new DisplayWhiteBalanceController( brightnessSensor, brightnessFilter, colorTemperatureSensor, colorTemperatureFilter, throttler, displayWhiteBalanceLowLightAmbientBrightnesses, - displayWhiteBalanceLowLightAmbientBiases, lowLightAmbientColorTemperature, + displayWhiteBalanceLowLightAmbientBrightnessesStrong, + displayWhiteBalanceLowLightAmbientBiases, + displayWhiteBalanceLowLightAmbientBiasesStrong, lowLightAmbientColorTemperature, + lowLightAmbientColorTemperatureStrong, displayWhiteBalanceHighLightAmbientBrightnesses, - displayWhiteBalanceHighLightAmbientBiases, highLightAmbientColorTemperature, + displayWhiteBalanceHighLightAmbientBrightnessesStrong, + displayWhiteBalanceHighLightAmbientBiases, + displayWhiteBalanceHighLightAmbientBiasesStrong, highLightAmbientColorTemperature, + highLightAmbientColorTemperatureStrong, ambientColorTemperatures, displayColorTemperatures, strongAmbientColorTemperatures, strongDisplayColorTemperatures, lightModeAllowed); brightnessSensor.setCallbacks(controller); diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java index d764ec41b3b9..9172dc02a4ac 100644 --- a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java +++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java @@ -32,6 +32,10 @@ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { private int mInitialAudioStatusRetriesLeft = 2; + // Flag to notify AudioService of the next audio status reported, + // regardless of whether the audio status changed. + private boolean mForceNextAudioStatusUpdate = false; + private static final int STATE_WAIT_FOR_INITIAL_AUDIO_STATUS = 1; private static final int STATE_MONITOR_AUDIO_STATUS = 2; @@ -70,6 +74,17 @@ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { return false; } + + /** + * If AVB has been enabled, send <Give Audio Status> and notify AudioService of the response. + */ + void requestAndUpdateAudioStatus() { + if (mState == STATE_MONITOR_AUDIO_STATUS) { + mForceNextAudioStatusUpdate = true; + sendGiveAudioStatus(); + } + } + private boolean handleReportAudioStatus(HdmiCecMessage cmd) { if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) { return false; @@ -89,12 +104,15 @@ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { localDevice().getService().enableAbsoluteVolumeBehavior(audioStatus); mState = STATE_MONITOR_AUDIO_STATUS; } else if (mState == STATE_MONITOR_AUDIO_STATUS) { - if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) { + if (mForceNextAudioStatusUpdate + || audioStatus.getVolume() != mLastAudioStatus.getVolume()) { localDevice().getService().notifyAvbVolumeChange(audioStatus.getVolume()); } - if (audioStatus.getMute() != mLastAudioStatus.getMute()) { + if (mForceNextAudioStatusUpdate + || audioStatus.getMute() != mLastAudioStatus.getMute()) { localDevice().getService().notifyAvbMuteChange(audioStatus.getMute()); } + mForceNextAudioStatusUpdate = false; } mLastAudioStatus = audioStatus; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 207d38eb17ac..0671464a1ed4 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -1048,6 +1048,19 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { } /** + * If AVB has been enabled, request the System Audio device's audio status and notify + * AudioService of its response. + */ + @ServiceThreadOnly + void requestAndUpdateAvbAudioStatus() { + assertRunOnServiceThread(); + for (AbsoluteVolumeAudioStatusAction action : + getActions(AbsoluteVolumeAudioStatusAction.class)) { + action.requestAndUpdateAudioStatus(); + } + } + + /** * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things * in parallel: send <Give Features> (to get <Report Features> in response), * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response). diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 5abb2b5ae861..99fa3a3271aa 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -4661,6 +4661,13 @@ public class HdmiControlService extends SystemService { // same keycode for all three mute options. keyCode = KeyEvent.KEYCODE_VOLUME_MUTE; break; + case AudioManager.ADJUST_SAME: + // Query the current audio status of the Audio System and display UI for it + // Only for TVs, because Playback devices don't display UI when using AVB + if (tv() != null) { + tv().requestAndUpdateAvbAudioStatus(); + } + return; default: return; } diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/FocusEventDebugView.java index 02e0eb053c88..39519ef79b1a 100644 --- a/services/core/java/com/android/server/input/FocusEventDebugView.java +++ b/services/core/java/com/android/server/input/FocusEventDebugView.java @@ -28,7 +28,7 @@ import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.Typeface; -import android.util.Log; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.Slog; import android.util.TypedValue; @@ -38,22 +38,26 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RoundedCorner; import android.view.View; +import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.animation.AccelerateInterpolator; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; /** * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on * the screen. */ -class FocusEventDebugView extends LinearLayout { +class FocusEventDebugView extends RelativeLayout { private static final String TAG = FocusEventDebugView.class.getSimpleName(); @@ -80,18 +84,24 @@ class FocusEventDebugView extends LinearLayout { private PressedKeyContainer mPressedKeyContainer; @Nullable private PressedKeyContainer mPressedModifierContainer; + private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory; + @Nullable + private RotaryInputValueView mRotaryInputValueView; - FocusEventDebugView(Context c, InputManagerService service) { + @VisibleForTesting + FocusEventDebugView(Context c, InputManagerService service, + Supplier<RotaryInputValueView> rotaryInputValueViewFactory) { super(c); setFocusableInTouchMode(true); mService = service; + mRotaryInputValueViewFactory = rotaryInputValueViewFactory; final var dm = mContext.getResources().getDisplayMetrics(); mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); + } - setOrientation(HORIZONTAL); - setLayoutDirection(LAYOUT_DIRECTION_RTL); - setGravity(Gravity.START | Gravity.BOTTOM); + FocusEventDebugView(Context c, InputManagerService service) { + this(c, service, () -> new RotaryInputValueView(c)); } @Override @@ -100,13 +110,13 @@ class FocusEventDebugView extends LinearLayout { final RoundedCorner bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); - if (bottomLeft != null) { + if (bottomLeft != null && !insets.isRound()) { paddingBottom = bottomLeft.getRadius(); } final RoundedCorner bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); - if (bottomRight != null) { + if (bottomRight != null && !insets.isRound()) { paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); } @@ -151,7 +161,7 @@ class FocusEventDebugView extends LinearLayout { } mPressedKeyContainer = new PressedKeyContainer(mContext); - mPressedKeyContainer.setOrientation(HORIZONTAL); + mPressedKeyContainer.setOrientation(LinearLayout.HORIZONTAL); mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); final var scroller = new HorizontalScrollView(mContext); @@ -160,15 +170,23 @@ class FocusEventDebugView extends LinearLayout { scroller.addOnLayoutChangeListener( (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); scroller.setHorizontalFadingEdgeEnabled(true); - addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1)); + LayoutParams scrollerLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + scrollerLayoutParams.addRule(ALIGN_PARENT_BOTTOM); + scrollerLayoutParams.addRule(ALIGN_PARENT_RIGHT); + addView(scroller, scrollerLayoutParams); mPressedModifierContainer = new PressedKeyContainer(mContext); - mPressedModifierContainer.setOrientation(VERTICAL); + mPressedModifierContainer.setOrientation(LinearLayout.VERTICAL); mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); - addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + LayoutParams modifierLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + modifierLayoutParams.addRule(ALIGN_PARENT_BOTTOM); + modifierLayoutParams.addRule(ALIGN_PARENT_LEFT); + modifierLayoutParams.addRule(LEFT_OF, scroller.getId()); + addView(mPressedModifierContainer, modifierLayoutParams); } - private void handleUpdateShowRotaryInput(boolean enabled) { + @VisibleForTesting + void handleUpdateShowRotaryInput(boolean enabled) { if (enabled == showRotaryInput()) { return; } @@ -176,10 +194,18 @@ class FocusEventDebugView extends LinearLayout { if (!enabled) { mFocusEventDebugGlobalMonitor.dispose(); mFocusEventDebugGlobalMonitor = null; + removeView(mRotaryInputValueView); + mRotaryInputValueView = null; return; } mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService); + + mRotaryInputValueView = mRotaryInputValueViewFactory.get(); + LayoutParams valueLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + valueLayoutParams.addRule(CENTER_HORIZONTAL); + valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM); + addView(mRotaryInputValueView, valueLayoutParams); } /** Report a key event to the debug view. */ @@ -242,14 +268,14 @@ class FocusEventDebugView extends LinearLayout { keyEvent.recycle(); } - private void handleRotaryInput(MotionEvent motionEvent) { + @VisibleForTesting + void handleRotaryInput(MotionEvent motionEvent) { if (!showRotaryInput()) { return; } float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); - // TODO(b/286086154): replace log with visualization. - Log.d(TAG, "ROTARY INPUT: " + String.valueOf(scrollAxisValue)); + mRotaryInputValueView.updateValue(scrollAxisValue); motionEvent.recycle(); } @@ -308,7 +334,14 @@ class FocusEventDebugView extends LinearLayout { /** Determine whether to show rotary input by checking one of the rotary-related objects. */ private boolean showRotaryInput() { - return mFocusEventDebugGlobalMonitor != null; + return mRotaryInputValueView != null; + } + + /** + * Converts a dimension in scaled pixel units to integer display pixels. + */ + private static int applyDimensionSp(int dimensionSp, DisplayMetrics dm) { + return (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, dimensionSp, dm); } private static class PressedKeyView extends TextView { @@ -419,4 +452,66 @@ class FocusEventDebugView extends LinearLayout { invalidate(); } } + + /** Draws the most recent rotary input value and indicates whether the source is active. */ + @VisibleForTesting + static class RotaryInputValueView extends TextView { + + private static final int INACTIVE_TEXT_COLOR = 0xffff00ff; + private static final int ACTIVE_TEXT_COLOR = 0xff420f28; + private static final int TEXT_SIZE_SP = 8; + private static final int SIDE_PADDING_SP = 4; + /** Determines how long the active status lasts. */ + private static final int ACTIVE_STATUS_DURATION = 250 /* milliseconds */; + private static final ColorFilter ACTIVE_BACKGROUND_FILTER = + new ColorMatrixColorFilter(new float[]{ + 0, 0, 0, 0, 255, // red + 0, 0, 0, 0, 0, // green + 0, 0, 0, 0, 255, // blue + 0, 0, 0, 0, 200 // alpha + }); + + private final Runnable mUpdateActivityStatusCallback = () -> updateActivityStatus(false); + private final float mScaledVerticalScrollFactor; + + @VisibleForTesting + RotaryInputValueView(Context c) { + super(c); + + DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); + mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor(); + + setText(getFormattedValue(0)); + setTextColor(INACTIVE_TEXT_COLOR); + setTextSize(applyDimensionSp(TEXT_SIZE_SP, dm)); + setPaddingRelative(applyDimensionSp(SIDE_PADDING_SP, dm), 0, + applyDimensionSp(SIDE_PADDING_SP, dm), 0); + setTypeface(null, Typeface.BOLD); + setBackgroundResource(R.drawable.focus_event_rotary_input_background); + } + + void updateValue(float value) { + removeCallbacks(mUpdateActivityStatusCallback); + + setText(getFormattedValue(value * mScaledVerticalScrollFactor)); + + updateActivityStatus(true); + postDelayed(mUpdateActivityStatusCallback, ACTIVE_STATUS_DURATION); + } + + @VisibleForTesting + void updateActivityStatus(boolean active) { + if (active) { + setTextColor(ACTIVE_TEXT_COLOR); + getBackground().setColorFilter(ACTIVE_BACKGROUND_FILTER); + } else { + setTextColor(INACTIVE_TEXT_COLOR); + getBackground().clearColorFilter(); + } + } + + private static String getFormattedValue(float value) { + return String.format("%s%.1f", value < 0 ? "-" : "+", Math.abs(value)); + } + } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 2fc48294ae70..8e7baf26984a 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -5959,6 +5959,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer.dump(pw); p.println(" mInFullscreenMode=" + mInFullscreenMode); p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive); + p.println(" ENABLE_HIDE_IME_CAPTION_BAR=" + + InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR); p.println(" mSettingsObserver=" + mSettingsObserver); p.println(" mStylusIds=" + (mStylusIds != null ? Arrays.toString(mStylusIds.toArray()) : "")); diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 3a20cd962667..f2242bf48dcd 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -2188,11 +2188,10 @@ public class MediaSessionService extends SystemService implements Monitor { MediaSessionRecordImpl session = isGlobalPriorityActiveLocked() ? mGlobalPrioritySession : mCurrentFullUserRecord.mPriorityStack.getDefaultVolumeSession(); - boolean preferSuggestedStream = false; - if (isValidLocalStreamType(suggestedStream) - && AudioSystem.isStreamActive(suggestedStream, 0)) { - preferSuggestedStream = true; - } + boolean preferSuggestedStream = + isValidLocalStreamType(suggestedStream) + && AudioSystem.isStreamActive(suggestedStream, 0); + if (session == null || preferSuggestedStream) { if (DEBUG_KEY_EVENT) { Log.d(TAG, "Adjusting suggestedStream=" + suggestedStream + " by " + direction diff --git a/services/core/java/com/android/server/net/watchlist/OWNERS b/services/core/java/com/android/server/net/watchlist/OWNERS index a3d4b85367cf..d0c4e553ad8c 100644 --- a/services/core/java/com/android/server/net/watchlist/OWNERS +++ b/services/core/java/com/android/server/net/watchlist/OWNERS @@ -1,3 +1,2 @@ -rickywai@google.com alanstokes@google.com simonjw@google.com diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index b2d3fca16112..100c63863f7f 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -25,8 +25,6 @@ import static android.service.notification.NotificationListenerService.Ranking.U import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.IActivityManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; @@ -48,6 +46,7 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; +import android.os.Trace; import android.os.UserHandle; import android.os.VibrationEffect; import android.provider.Settings; @@ -98,8 +97,7 @@ public final class NotificationRecord { // the period after which a notification is updated where it can make sound private static final int MAX_SOUND_DELAY_MS = 2000; private final StatusBarNotification sbn; - IActivityManager mAm; - UriGrantsManagerInternal mUgmInternal; + private final UriGrantsManagerInternal mUgmInternal; final int mTargetSdkVersion; final int mOriginalFlags; private final Context mContext; @@ -223,7 +221,6 @@ public final class NotificationRecord { this.sbn = sbn; mTargetSdkVersion = LocalServices.getService(PackageManagerInternal.class) .getPackageTargetSdkVersion(sbn.getPackageName()); - mAm = ActivityManager.getService(); mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); mOriginalFlags = sbn.getNotification().flags; mRankingTimeMs = calculateRankingTimeMs(0L); @@ -1387,18 +1384,27 @@ public final class NotificationRecord { * Collect all {@link Uri} that should have permission granted to whoever * will be rendering it. */ - protected void calculateGrantableUris() { - final Notification notification = getNotification(); - notification.visitUris((uri) -> { - visitGrantableUri(uri, false, false); - }); - - if (notification.getChannelId() != null) { - NotificationChannel channel = getChannel(); - if (channel != null) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + private void calculateGrantableUris() { + Trace.beginSection("NotificationRecord.calculateGrantableUris"); + try { + // We can't grant URI permissions from system. + final int sourceUid = getSbn().getUid(); + if (sourceUid == android.os.Process.SYSTEM_UID) return; + + final Notification notification = getNotification(); + notification.visitUris((uri) -> { + visitGrantableUri(uri, false, false); + }); + + if (notification.getChannelId() != null) { + NotificationChannel channel = getChannel(); + if (channel != null) { + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + } } + } finally { + Trace.endSection(); } } @@ -1413,13 +1419,14 @@ public final class NotificationRecord { private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - // We can't grant Uri permissions from system - final int sourceUid = getSbn().getUid(); - if (sourceUid == android.os.Process.SYSTEM_UID) return; + if (mGrantableUris != null && mGrantableUris.contains(uri)) { + return; // already verified this URI + } + final int sourceUid = getSbn().getUid(); final long ident = Binder.clearCallingIdentity(); try { - // This will throw SecurityException if caller can't grant + // This will throw a SecurityException if the caller can't grant. mUgmInternal.checkGrantUriPermission(sourceUid, null, ContentProvider.getUriWithoutUserId(uri), Intent.FLAG_GRANT_READ_URI_PERMISSION, diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index 6e7560563c8d..f9876299e8e0 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -487,6 +487,8 @@ public interface Computer extends PackageDataSnapshot { boolean isPackageSuspendedForUser(@NonNull String packageName, @UserIdInt int userId); + boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId); + boolean isSuspendingAnyPackages(@NonNull String suspendingPackage, @UserIdInt int userId); @NonNull diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 7d878ece3176..1cfc7d76919a 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -4925,8 +4925,8 @@ public class ComputerEngine implements Computer { } } - @Override - public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) { + private PackageUserStateInternal getUserStageOrDefaultForUser(@NonNull String packageName, + int userId) { final int callingUid = Binder.getCallingUid(); enforceCrossUserPermission(callingUid, userId, true /* requireFullPermission */, false /* checkShell */, "isPackageSuspendedForUser for user " + userId); @@ -4934,7 +4934,17 @@ public class ComputerEngine implements Computer { if (ps == null || shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)) { throw new IllegalArgumentException("Unknown target package: " + packageName); } - return ps.getUserStateOrDefault(userId).isSuspended(); + return ps.getUserStateOrDefault(userId); + } + + @Override + public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) { + return getUserStageOrDefaultForUser(packageName, userId).isSuspended(); + } + + @Override + public boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) { + return getUserStageOrDefaultForUser(packageName, userId).isQuarantined(); } @Override diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index fd47846af9ef..76203ac7650d 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -955,6 +955,13 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Override @Deprecated + public final boolean isPackageQuarantinedForUser(@NonNull String packageName, + @UserIdInt int userId) { + return snapshot().isPackageQuarantinedForUser(packageName, userId); + } + + @Override + @Deprecated public final boolean isSafeMode() { // allow instant applications return mService.getSafeMode(); diff --git a/services/core/java/com/android/server/pm/MovePackageHelper.java b/services/core/java/com/android/server/pm/MovePackageHelper.java index 1a5591c98269..01c27348db94 100644 --- a/services/core/java/com/android/server/pm/MovePackageHelper.java +++ b/services/core/java/com/android/server/pm/MovePackageHelper.java @@ -49,8 +49,8 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.os.storage.StorageManager; +import android.os.storage.StorageManagerInternal; import android.os.storage.VolumeInfo; -import android.text.TextUtils; import android.util.MathUtils; import android.util.Slog; import android.util.SparseIntArray; @@ -64,6 +64,7 @@ import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.PackageStateUtils; import java.io.File; +import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -220,9 +221,7 @@ public final class MovePackageHelper { } try { - for (int index = 0; index < installedUserIds.length; index++) { - prepareUserDataForVolumeIfRequired(volumeUuid, installedUserIds[index], storage); - } + prepareUserStorageForMove(currentVolumeUuid, volumeUuid, installedUserIds); } catch (RuntimeException e) { freezer.close(); throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR, @@ -376,27 +375,20 @@ public final class MovePackageHelper { return true; } - private void prepareUserDataForVolumeIfRequired(String volumeUuid, int userId, - StorageManager storageManager) { - if (TextUtils.isEmpty(volumeUuid) - || doesDataDirectoryExistForUser(volumeUuid, userId)) { - return; - } + private void prepareUserStorageForMove(String fromVolumeUuid, String toVolumeUuid, + int[] userIds) { if (DEBUG_INSTALL) { - Slog.d(TAG, "Preparing user directories for user u" + userId + " for UUID " - + volumeUuid); + Slog.d(TAG, "Preparing user directories before moving app, from UUID " + fromVolumeUuid + + " to UUID " + toVolumeUuid); } - final UserInfo user = mPm.mUserManager.getUserInfo(userId); - if (user == null) return; - // This call is same as StorageEventHelper#loadPrivatePackagesInner which prepares - // the storage before reconciling apps - storageManager.prepareUserStorage(volumeUuid, user.id, user.serialNumber, - StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE); - } - - private boolean doesDataDirectoryExistForUser(String uuid, int userId) { - final File userDirectoryFile = Environment.getDataUserCeDirectory(uuid, userId); - return userDirectoryFile != null && userDirectoryFile.exists(); + final StorageManagerInternal smInternal = + mPm.mInjector.getLocalService(StorageManagerInternal.class); + final ArrayList<UserInfo> users = new ArrayList<>(); + for (int userId : userIds) { + final UserInfo user = mPm.mUserManager.getUserInfo(userId); + users.add(user); + } + smInternal.prepareUserStorageForMove(fromVolumeUuid, toVolumeUuid, users); } public static class MoveCallbacks extends Handler { diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 6efd06762678..651845e71924 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -755,9 +755,7 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { @Override public boolean isPackageQuarantined(@NonNull String packageName, @UserIdInt int userId) { - final PackageStateInternal packageState = getPackageStateInternal(packageName); - return (packageState == null) ? false - : packageState.getUserStateOrDefault(userId).isQuarantined(); + return snapshot().isPackageQuarantinedForUser(packageName, userId); } @NonNull diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index ceae1fe3bcb9..8bdbe04ec4e6 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -226,6 +226,8 @@ class PackageManagerShellCommand extends ShellCommand { return runPath(); case "dump": return runDump(); + case "dump-package": + return runDumpPackage(); case "list": return runList(); case "gc": @@ -978,6 +980,7 @@ class PackageManagerShellCommand extends ShellCommand { boolean listInstaller = false; boolean showUid = false; boolean showVersionCode = false; + boolean listQuarantinedOnly = false; boolean listApexOnly = false; boolean showStopped = false; int uid = -1; @@ -1008,6 +1011,9 @@ class PackageManagerShellCommand extends ShellCommand { case "-s": listSystem = true; break; + case "-q": + listQuarantinedOnly = true; + break; case "-U": showUid = true; break; @@ -1093,6 +1099,10 @@ class PackageManagerShellCommand extends ShellCommand { || (listApexOnly && !isApex)) { continue; } + if (listQuarantinedOnly && !mInterface.isPackageQuarantinedForUser(info.packageName, + translatedUserId)) { + continue; + } String name = null; if (showSdks) { @@ -3598,6 +3608,23 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runDumpPackage() { + String pkg = getNextArg(); + if (pkg == null) { + getErrPrintWriter().println("Error: no package specified"); + return 1; + } + try { + ((IBinder) mInterface).dump(getOutFileDescriptor(), new String[]{pkg}); + } catch (Throwable e) { + PrintWriter pw = getErrPrintWriter(); + pw.println("Failure dumping service:"); + e.printStackTrace(pw); + pw.flush(); + } + return 0; + } + private int runSetHarmfulAppWarning() throws RemoteException { int userId = UserHandle.USER_CURRENT; @@ -4282,6 +4309,9 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" dump PACKAGE"); pw.println(" Print various system state associated with the given PACKAGE."); pw.println(""); + pw.println(" dump-package PACKAGE"); + pw.println(" Print package manager state associated with the given PACKAGE."); + pw.println(""); pw.println(" has-feature FEATURE_NAME [version]"); pw.println(" Prints true and returns exit status 0 when system has a FEATURE_NAME,"); pw.println(" otherwise prints false and returns exit status 1"); @@ -4299,7 +4329,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" Options:"); pw.println(" -v: shows the location of the library in the device's filesystem"); pw.println(""); - pw.println(" list packages [-f] [-d] [-e] [-s] [-3] [-i] [-l] [-u] [-U] "); + pw.println(" list packages [-f] [-d] [-e] [-s] [-q] [-3] [-i] [-l] [-u] [-U] "); pw.println(" [--show-versioncode] [--apex-only] [--factory-only]"); pw.println(" [--uid UID] [--user USER_ID] [FILTER]"); pw.println(" Prints all packages; optionally only those whose name contains"); @@ -4309,6 +4339,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" -d: filter to only show disabled packages"); pw.println(" -e: filter to only show enabled packages"); pw.println(" -s: filter to only show system packages"); + pw.println(" -q: filter to only show quarantined packages"); pw.println(" -3: filter to only show third party packages"); pw.println(" -i: see the installer for the packages"); pw.println(" -l: ignored (used for compatibility with older releases)"); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 385dfcb8e4ec..f2797eb48305 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -4907,6 +4907,11 @@ public class UserManagerService extends IUserManager.Stub { USER_OPERATION_ERROR_UNKNOWN); } } + if (isMainUser && getMainUserIdUnchecked() != UserHandle.USER_NULL) { + throwCheckedUserOperationException( + "Cannot add user with FLAG_MAIN as main user already exists.", + UserManager.USER_OPERATION_ERROR_MAX_USERS); + } if (!preCreate && !canAddMoreUsersOfType(userTypeDetails)) { throwCheckedUserOperationException( "Cannot add more users of type " + userType diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3125518a24d5..cba215ad23fd 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -224,6 +224,9 @@ import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; @@ -2690,6 +2693,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (isTransferringSplashScreen()) { return true; } + // Only do transfer after transaction has done when starting window exist. + if (mStartingData != null && mStartingData.mWaitForSyncTransactionCommit) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_COPY_TO_CLIENT; + return true; + } requestCopySplashScreen(); return isTransferringSplashScreen(); } @@ -2850,11 +2858,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingData == null) { return; } - mStartingData.mWaitForSyncTransactionCommit = false; - if (mStartingData.mRemoveAfterTransaction) { - mStartingData.mRemoveAfterTransaction = false; - removeStartingWindowAnimation(mStartingData.mPrepareRemoveAnimation); + final StartingData lastData = mStartingData; + lastData.mWaitForSyncTransactionCommit = false; + if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) { + removeStartingWindowAnimation(lastData.mPrepareRemoveAnimation); + } else if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_COPY_TO_CLIENT) { + removeStartingWindow(); } + lastData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; } void removeStartingWindowAnimation(boolean prepareAnimation) { @@ -2881,7 +2892,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingData != null) { if (mStartingData.mWaitForSyncTransactionCommit || mTransitionController.inCollectingTransition(startingWindow)) { - mStartingData.mRemoveAfterTransaction = true; + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY; mStartingData.mPrepareRemoveAnimation = prepareAnimation; return; } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index ba242ecd0ec3..01786becda61 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -969,8 +969,10 @@ final class LetterboxUiController { final Rect innerFrame = hasInheritedLetterboxBehavior() ? mActivityRecord.getBounds() : w.getFrame(); mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint); - // We need to notify Shell that letterbox position has changed. - mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + if (mDoubleTapEvent) { + // We need to notify Shell that letterbox position has changed. + mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + } } else if (mLetterbox != null) { mLetterbox.hide(); } @@ -1242,6 +1244,7 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().right < x) { // Moving to the next stop on the right side of the app window: left > center > right. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop( @@ -1252,8 +1255,8 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } - mDoubleTapEvent = true; // TODO(197549949): Add animation for transition. mActivityRecord.recomputeConfiguration(); } @@ -1281,6 +1284,7 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().bottom < y) { // Moving to the next stop on the bottom side of the app window: top > center > bottom. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop( @@ -1291,8 +1295,8 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } - mDoubleTapEvent = true; // TODO(197549949): Add animation for transition. mActivityRecord.recomputeConfiguration(); } diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index 0af9fe930d83..26abe51c8c34 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -18,4 +18,4 @@ rgl@google.com yunfanc@google.com per-file BackgroundActivityStartController.java = set noparent -per-file BackgroundActivityStartController.java = brufino@google.com, ogunwale@google.com, louischang@google.com, lus@google.com, rickywai@google.com +per-file BackgroundActivityStartController.java = brufino@google.com, ogunwale@google.com, louischang@google.com, lus@google.com diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 34806bd023a0..a23547ef1d5b 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import android.annotation.IntDef; + import com.android.server.wm.StartingSurfaceController.StartingSurface; /** @@ -23,6 +25,20 @@ import com.android.server.wm.StartingSurfaceController.StartingSurface; */ public abstract class StartingData { + /** Nothing need to do after transaction */ + static final int AFTER_TRANSACTION_IDLE = 0; + /** Remove the starting window directly after transaction done. */ + static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1; + /** Do copy splash screen to client after transaction done. */ + static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2; + + @IntDef(prefix = { "AFTER_TRANSACTION" }, value = { + AFTER_TRANSACTION_IDLE, + AFTER_TRANSACTION_REMOVE_DIRECTLY, + AFTER_TRANSACTION_COPY_TO_CLIENT, + }) + @interface AfterTransaction {} + protected final WindowManagerService mService; protected final int mTypeParams; @@ -60,7 +76,7 @@ public abstract class StartingData { * This starting window should be removed after applying the start transaction of transition, * which ensures the app window has shown. */ - boolean mRemoveAfterTransaction; + @AfterTransaction int mRemoveAfterTransaction; /** Whether to prepare the removal animation. */ boolean mPrepareRemoveAnimation; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 69eddb9fe105..43430dd1eed0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3459,6 +3459,8 @@ class Task extends TaskFragment { info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; + info.isUserFullscreenOverrideEnabled = top != null + && top.mLetterboxUiController.shouldApplyUserFullscreenOverride(); info.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap(); if (info.isLetterboxDoubleTapEnabled) { info.topActivityLetterboxWidth = top.getBounds().width(); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 57b6e379c54c..6dc896a92e52 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1175,7 +1175,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { } final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options, - boolean deferPause) { + boolean skipPause) { ActivityRecord next = topRunningActivity(true /* focusableOnly */); if (next == null || !next.canResumeByCompat()) { return false; @@ -1183,11 +1183,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { next.delayedResume = false; - // If we are currently pausing an activity, then don't do anything until that is done. - final boolean allPausedComplete = mRootWindowContainer.allPausedActivitiesComplete(); - if (!allPausedComplete) { - ProtoLog.v(WM_DEBUG_STATES, - "resumeTopActivity: Skip resume: some activity pausing."); + if (!skipPause && !mRootWindowContainer.allPausedActivitiesComplete()) { + // If we aren't skipping pause, then we have to wait for currently pausing activities. + ProtoLog.v(WM_DEBUG_STATES, "resumeTopActivity: Skip resume: some activity pausing."); return false; } @@ -1251,7 +1249,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { lastResumed = lastFocusedRootTask.getTopResumedActivity(); } - boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next); + boolean pausing = !skipPause && taskDisplayArea.pauseBackTasks(next); if (mResumedActivity != null) { ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Pausing %s", mResumedActivity); pausing |= startPausing(mTaskSupervisor.mUserLeaving, false /* uiSleeping */, diff --git a/services/permission/java/com/android/server/permission/access/immutable/IntMapExtensions.kt b/services/permission/java/com/android/server/permission/access/immutable/IntMapExtensions.kt index ed7f0af9299f..9aa0a4182eb7 100644 --- a/services/permission/java/com/android/server/permission/access/immutable/IntMapExtensions.kt +++ b/services/permission/java/com/android/server/permission/access/immutable/IntMapExtensions.kt @@ -77,7 +77,7 @@ inline fun <T> MutableIntMap<T>.getOrPut(key: Int, defaultValue: () -> T): T { } operator fun <T> MutableIntMap<T>.minusAssign(key: Int) { - array.remove(key) + array.remove(key).also { array.gc() } } fun <T> MutableIntMap<T>.putWithDefault(key: Int, value: T, defaultValue: T): T { diff --git a/services/permission/java/com/android/server/permission/access/immutable/IntReferenceMapExtensions.kt b/services/permission/java/com/android/server/permission/access/immutable/IntReferenceMapExtensions.kt index b4de5d164e3b..1ed4f8a777d2 100644 --- a/services/permission/java/com/android/server/permission/access/immutable/IntReferenceMapExtensions.kt +++ b/services/permission/java/com/android/server/permission/access/immutable/IntReferenceMapExtensions.kt @@ -77,7 +77,7 @@ inline fun <I : Immutable<M>, M : I> MutableIntReferenceMap<I, M>.mutateOrPut( } operator fun <I : Immutable<M>, M : I> MutableIntReferenceMap<I, M>.minusAssign(key: Int) { - array.remove(key) + array.remove(key).also { array.gc() } } operator fun <I : Immutable<M>, M : I> MutableIntReferenceMap<I, M>.set(key: Int, value: M) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java index f975b6fd1d6f..183a84de0eb0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java @@ -64,7 +64,9 @@ public final class AmbientLuxTest { private static final int AMBIENT_COLOR_TYPE = 20705; private static final String AMBIENT_COLOR_TYPE_STR = "colorSensoryDensoryDoc"; private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE = 5432.1f; + private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG = 5555.5f; private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE = 3456.7f; + private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG = 3333.3f; private Handler mHandler = new Handler(Looper.getMainLooper()); private Sensor mLightSensor; @@ -78,6 +80,10 @@ public final class AmbientLuxTest { @Mock private TypedArray mBiases; @Mock private TypedArray mHighLightBrightnesses; @Mock private TypedArray mHighLightBiases; + @Mock private TypedArray mBrightnessesStrong; + @Mock private TypedArray mBiasesStrong; + @Mock private TypedArray mHighLightBrightnessesStrong; + @Mock private TypedArray mHighLightBiasesStrong; @Mock private TypedArray mAmbientColorTemperatures; @Mock private TypedArray mDisplayColorTemperatures; @Mock private TypedArray mStrongAmbientColorTemperatures; @@ -108,6 +114,10 @@ public final class AmbientLuxTest { LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE); mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperature, HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE); + mockResourcesFloat(R.dimen.config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong, + LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG); + mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong, + HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG); when(mResourcesSpy.obtainTypedArray( R.array.config_displayWhiteBalanceAmbientColorTemperatures)) .thenReturn(mAmbientColorTemperatures); @@ -133,6 +143,18 @@ public final class AmbientLuxTest { when(mResourcesSpy.obtainTypedArray( R.array.config_displayWhiteBalanceHighLightAmbientBiases)) .thenReturn(mHighLightBiases); + when(mResourcesSpy.obtainTypedArray( + R.array.config_displayWhiteBalanceLowLightAmbientBrightnessesStrong)) + .thenReturn(mBrightnessesStrong); + when(mResourcesSpy.obtainTypedArray( + R.array.config_displayWhiteBalanceLowLightAmbientBiasesStrong)) + .thenReturn(mBiasesStrong); + when(mResourcesSpy.obtainTypedArray( + R.array.config_displayWhiteBalanceHighLightAmbientBrightnessesStrong)) + .thenReturn(mHighLightBrightnessesStrong); + when(mResourcesSpy.obtainTypedArray( + R.array.config_displayWhiteBalanceHighLightAmbientBiasesStrong)) + .thenReturn(mHighLightBiasesStrong); mockThrottler(); LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class); LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class, @@ -388,8 +410,8 @@ public final class AmbientLuxTest { public void testStrongMode() { final float lowerBrightness = 10.0f; final float upperBrightness = 50.0f; - setBrightnesses(lowerBrightness, upperBrightness); - setBiases(0.0f, 1.0f); + setBrightnessesStrong(lowerBrightness, upperBrightness); + setBiasesStrong(0.0f, 1.0f); final int ambientColorTempLow = 6000; final int ambientColorTempHigh = 8000; final int displayColorTempLow = 6400; @@ -413,7 +435,7 @@ public final class AmbientLuxTest { setEstimatedBrightnessAndUpdate(controller, mix(lowerBrightness, upperBrightness, brightnessFraction)); assertEquals(controller.mPendingAmbientColorTemperature, - mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, + mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG, mix(displayColorTempLow, displayColorTempHigh, ambientTempFraction), brightnessFraction), ALLOWED_ERROR_DELTA); @@ -458,7 +480,7 @@ public final class AmbientLuxTest { assertEquals(-1.0f, controller.mPendingAmbientColorTemperature, 0); } - void mockThrottler() { + private void mockThrottler() { when(mResourcesSpy.getInteger( R.integer.config_displayWhiteBalanceDecreaseDebounce)).thenReturn(0); when(mResourcesSpy.getInteger( @@ -513,10 +535,18 @@ public final class AmbientLuxTest { setFloatArrayResource(mBrightnesses, vals); } + private void setBrightnessesStrong(float... vals) { + setFloatArrayResource(mBrightnessesStrong, vals); + } + private void setBiases(float... vals) { setFloatArrayResource(mBiases, vals); } + private void setBiasesStrong(float... vals) { + setFloatArrayResource(mBiasesStrong, vals); + } + private void setHighLightBrightnesses(float... vals) { setFloatArrayResource(mHighLightBrightnesses, vals); } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 78e5a42e6d4c..bdbf4ecbd346 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -63,6 +63,8 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; +import java.io.File; + /** * Run as {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserManagerServiceTest} */ @@ -107,6 +109,8 @@ public final class UserManagerServiceTest { .getTargetContext(); private final SparseArray<UserData> mUsers = new SparseArray<>(); + private File mTestDir; + private Context mSpiedContext; private @Mock PackageManagerService mMockPms; @@ -144,17 +148,23 @@ public final class UserManagerServiceTest { doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any()); // Must construct UserManagerService in the UiThread + mTestDir = new File(mRealContext.getDataDir(), "umstest"); + mTestDir.mkdirs(); mUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer, - mPackagesLock, mRealContext.getDataDir(), mUsers); + mPackagesLock, mTestDir, mUsers); mUmi = LocalServices.getService(UserManagerInternal.class); assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mUmi) .isNotNull(); } @After - public void resetUserManagerInternal() { + public void tearDown() { // LocalServices follows the "Highlander rule" - There can be only one! LocalServices.removeServiceForTest(UserManagerInternal.class); + + // Clean up test dir to remove persisted user files. + assertThat(deleteRecursive(mTestDir)).isTrue(); + mUsers.clear(); } @Test @@ -496,6 +506,14 @@ public final class UserManagerServiceTest { @Test public void testMainUser_hasNoCallsOrSMSRestrictionsByDefault() { + // Remove the main user so we can add another one + for (int i = 0; i < mUsers.size(); i++) { + UserData userData = mUsers.valueAt(i); + if (userData.info.isMain()) { + mUsers.delete(i); + break; + } + } UserInfo mainUser = mUms.createUserWithThrow("main user", USER_TYPE_FULL_SECONDARY, UserInfo.FLAG_FULL | UserInfo.FLAG_MAIN); @@ -621,6 +639,18 @@ public final class UserManagerServiceTest { userData.mLastEnteredForegroundTimeMillis = timeMillis; } + public boolean deleteRecursive(File file) { + if (file.isDirectory()) { + for (File item : file.listFiles()) { + boolean success = deleteRecursive(item); + if (!success) { + return false; + } + } + } + return file.delete(); + } + private static final class TestUserData extends UserData { @SuppressWarnings("deprecation") diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java index 99d66c5bda19..746fb53c7254 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java @@ -16,20 +16,42 @@ package com.android.server.biometrics; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static java.util.Collections.emptySet; + import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Resources; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.server.biometrics.sensors.BiometricNotification; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.File; + +@Presubmit +@SmallTest public class AuthenticationStatsCollectorTest { private AuthenticationStatsCollector mAuthenticationStatsCollector; @@ -40,47 +62,220 @@ public class AuthenticationStatsCollectorTest { private Context mContext; @Mock private Resources mResources; + @Mock + private PackageManager mPackageManager; + @Mock + private FingerprintManager mFingerprintManager; + @Mock + private FaceManager mFaceManager; + @Mock + private SharedPreferences mSharedPreferences; + @Mock + private BiometricNotification mBiometricNotification; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mContext.getResources()).thenReturn(mResources); - when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1)) - .thenReturn(FRR_THRESHOLD); + when(mResources.getFraction(eq(R.fraction.config_biometricNotificationFrrThreshold), + anyInt(), anyInt())).thenReturn(FRR_THRESHOLD); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + + when(mContext.getSystemServiceName(FingerprintManager.class)) + .thenReturn(Context.FINGERPRINT_SERVICE); + when(mContext.getSystemService(Context.FINGERPRINT_SERVICE)) + .thenReturn(mFingerprintManager); + when(mContext.getSystemServiceName(FaceManager.class)).thenReturn(Context.FACE_SERVICE); + when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager); + + when(mContext.getSharedPreferences(any(File.class), anyInt())) + .thenReturn(mSharedPreferences); + when(mSharedPreferences.getStringSet(anyString(), anySet())).thenReturn(emptySet()); mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, - 0 /* modality */); + 0 /* modality */, mBiometricNotification); } @Test public void authenticate_authenticationSucceeded_mapShouldBeUpdated() { // Assert that the user doesn't exist in the map initially. - assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)); + assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull(); - mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated*/); + mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated */); AuthenticationStats authenticationStats = mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1); - assertEquals(USER_ID_1, authenticationStats.getUserId()); - assertEquals(1, authenticationStats.getTotalAttempts()); - assertEquals(0, authenticationStats.getRejectedAttempts()); - assertEquals(0, authenticationStats.getEnrollmentNotifications()); + assertThat(authenticationStats.getUserId()).isEqualTo(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); } @Test public void authenticate_authenticationFailed_mapShouldBeUpdated() { // Assert that the user doesn't exist in the map initially. - assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)); + assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull(); - mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated*/); + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); AuthenticationStats authenticationStats = mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1); - assertEquals(USER_ID_1, authenticationStats.getUserId()); - assertEquals(1, authenticationStats.getTotalAttempts()); - assertEquals(1, authenticationStats.getRejectedAttempts()); - assertEquals(0, authenticationStats.getEnrollmentNotifications()); + + assertThat(authenticationStats.getUserId()).isEqualTo(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(1); + assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0); + } + + @Test + public void authenticate_frrNotExceeded_notificationNotExceeded_shouldNotSendNotification() { + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 40 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */)); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + } + + @Test + public void authenticate_frrExceeded_notificationExceeded_shouldNotSendNotification() { + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 2 /* enrollmentNotifications */, + 0 /* modality */)); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + } + + @Test + public void authenticate_frrExceeded_bothBiometricsEnrolled_shouldNotSendNotification() { + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + } + + @Test + public void authenticate_frrExceeded_singleModality_shouldNotSendNotification() { + + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that no notification should be sent. + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + } + + @Test + public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification() { + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that fingerprint enrollment notification should be sent. + verify(mBiometricNotification, times(1)) + .sendFpEnrollNotification(mContext); + verify(mBiometricNotification, never()).sendFaceEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); + } + + @Test + public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification() { + mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1, + new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */, + 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */)); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */); + + // Assert that fingerprint enrollment notification should be sent. + verify(mBiometricNotification, times(1)) + .sendFaceEnrollNotification(mContext); + verify(mBiometricNotification, never()).sendFpEnrollNotification(any()); + // Assert that data has been reset. + AuthenticationStats authenticationStats = mAuthenticationStatsCollector + .getAuthenticationStatsForUser(USER_ID_1); + assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0); + assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0); + assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java new file mode 100644 index 000000000000..dde2a3c2cfd7 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.File; +import java.util.List; +import java.util.Set; + +@Presubmit +@SmallTest +public class AuthenticationStatsPersisterTest { + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + private static final int USER_ID_1 = 1; + private static final int USER_ID_2 = 2; + private static final String USER_ID = "user_id"; + private static final String FACE_ATTEMPTS = "face_attempts"; + private static final String FACE_REJECTIONS = "face_rejections"; + private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; + private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; + private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; + private static final String KEY = "frr_stats"; + + @Mock + private Context mContext; + @Mock + private SharedPreferences mSharedPreferences; + @Mock + private SharedPreferences.Editor mEditor; + private AuthenticationStatsPersister mAuthenticationStatsPersister; + + @Captor + private ArgumentCaptor<Set<String>> mStringSetArgumentCaptor; + + @Before + public void setUp() { + when(mContext.getSharedPreferences(any(File.class), anyInt())) + .thenReturn(mSharedPreferences); + when(mSharedPreferences.edit()).thenReturn(mEditor); + when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor); + + mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext); + } + + @Test + public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException { + AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, + 200 /* totalAttempts */, 20 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( + Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); + + List<AuthenticationStats> authenticationStatsList = + mAuthenticationStatsPersister.getAllFrrStats(BiometricsProtoEnums.MODALITY_FACE); + + assertThat(authenticationStatsList.size()).isEqualTo(2); + AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, + 0 /* totalAttempts */, 0 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + assertThat(authenticationStatsList).contains(stats1); + assertThat(authenticationStatsList).contains(expectedStats2); + } + + @Test + public void getAllFrrStats_fingerprint_shouldListAllFrrStats() throws JSONException { + // User 1 with fingerprint authentication stats. + AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, + 200 /* totalAttempts */, 20 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + // User 2 without fingerprint authentication stats. + AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( + Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); + + List<AuthenticationStats> authenticationStatsList = + mAuthenticationStatsPersister + .getAllFrrStats(BiometricsProtoEnums.MODALITY_FINGERPRINT); + + assertThat(authenticationStatsList.size()).isEqualTo(2); + AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, + 0 /* totalAttempts */, 0 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + assertThat(authenticationStatsList).contains(stats1); + assertThat(authenticationStatsList).contains(expectedStats2); + } + + @Test + public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException { + AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + + mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), + authenticationStats.getTotalAttempts(), + authenticationStats.getRejectedAttempts(), + authenticationStats.getEnrollmentNotifications(), + authenticationStats.getModality()); + + verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); + assertThat(mStringSetArgumentCaptor.getValue()) + .contains(buildFrrStats(authenticationStats)); + } + + @Test + public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException { + AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + + mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), + authenticationStats.getTotalAttempts(), + authenticationStats.getRejectedAttempts(), + authenticationStats.getEnrollmentNotifications(), + authenticationStats.getModality()); + + verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); + assertThat(mStringSetArgumentCaptor.getValue()) + .contains(buildFrrStats(authenticationStats)); + } + + @Test + public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException { + AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, + 500 /* totalAttempts */, 30 /* rejectedAttempts */, + 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( + Set.of(buildFrrStats(authenticationStats))); + + mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(), + newAuthenticationStats.getTotalAttempts(), + newAuthenticationStats.getRejectedAttempts(), + newAuthenticationStats.getEnrollmentNotifications(), + newAuthenticationStats.getModality()); + + verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); + assertThat(mStringSetArgumentCaptor.getValue()) + .contains(buildFrrStats(newAuthenticationStats)); + } + + @Test + public void persistFrrStats_existingUserWithFingerprint_faceAuthenticate_shouldUpdateRecord() + throws JSONException { + // User with fingerprint authentication stats. + AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, + 200 /* totalAttempts */, 20 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); + // The same user with face authentication stats. + AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, + 500 /* totalAttempts */, 30 /* rejectedAttempts */, + 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( + Set.of(buildFrrStats(authenticationStats))); + + mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(), + newAuthenticationStats.getTotalAttempts(), + newAuthenticationStats.getRejectedAttempts(), + newAuthenticationStats.getEnrollmentNotifications(), + newAuthenticationStats.getModality()); + + String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats)) + .put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications()) + .put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts()) + .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString(); + verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); + assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats); + } + + @Test + public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException { + AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, + 300 /* totalAttempts */, 10 /* rejectedAttempts */, + 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); + when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( + Set.of(buildFrrStats(authenticationStats))); + + mAuthenticationStatsPersister.removeFrrStats(USER_ID_1); + + verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); + assertThat(mStringSetArgumentCaptor.getValue()).doesNotContain(authenticationStats); + } + + private String buildFrrStats(AuthenticationStats authenticationStats) + throws JSONException { + if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FACE) { + return new JSONObject() + .put(USER_ID, authenticationStats.getUserId()) + .put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts()) + .put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts()) + .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) + .toString(); + } else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + return new JSONObject() + .put(USER_ID, authenticationStats.getUserId()) + .put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts()) + .put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts()) + .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) + .toString(); + } + return ""; + } +} diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index e9a7d85ae755..037637630b7a 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -67,6 +67,8 @@ import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public final class UpdatableFontDirTest { + private static final String LEGACY_FONTS_XML = "/system/etc/fonts.xml"; + /** * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files, * this test uses fake font files. A fake font file has its PostScript naem and revision as the @@ -140,7 +142,7 @@ public final class UpdatableFontDirTest { private List<File> mPreinstalledFontDirs; private final Supplier<Long> mCurrentTimeSupplier = () -> CURRENT_TIME; private final Function<Map<String, File>, FontConfig> mConfigSupplier = - (map) -> SystemFonts.getSystemFontConfig(map, 0, 0); + (map) -> SystemFonts.getSystemFontConfigForTesting(LEGACY_FONTS_XML, map, 0, 0); private FakeFontFileParser mParser; private FakeFsverityUtil mFakeFsverityUtil; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index 399655ffac55..e6d326a33415 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -92,11 +92,11 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { // Default Audio Status given by the System Audio device in its initial <Report Audio Status> // that triggers AVB being enabled - private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS = + protected static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS = new AudioStatus(50, false); // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVB - private static final VolumeInfo ENABLE_AVB_VOLUME_INFO = + protected static final VolumeInfo ENABLE_AVB_VOLUME_INFO = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()) .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume()) @@ -106,6 +106,8 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { private static final int EMPTY_FLAGS = 0; + protected static final int STREAM_MUSIC_MAX_VOLUME = 25; + protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService); protected abstract int getPhysicalAddress(); @@ -201,7 +203,7 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { Collections.singletonList(getAudioOutputDevice())); // Max volume of STREAM_MUSIC - mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25); + mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, STREAM_MUSIC_MAX_VOLUME); // Receive messages from devices to make sure they're registered in HdmiCecNetwork mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus( diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java index 024e36d62273..86647fc16826 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java @@ -321,4 +321,98 @@ public class TvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehaviorTest { getLogicalAddress(), getSystemAudioDeviceLogicalAddress(), Constants.AUDIO_VOLUME_STATUS_UNKNOWN)); } + + /** + * Tests that a volume adjustment command with direction ADJUST_SAME causes HdmiControlService + * to request the System Audio device's audio status, and notify AudioService of the + * audio status. + */ + @Test + public void avbEnabled_audioDeviceVolumeAdjusted_adjustSame_updatesAudioService() { + enableAbsoluteVolumeBehavior(); + mNativeWrapper.clearResultMessages(); + + // HdmiControlService receives a volume adjustment with direction ADJUST_SAME + mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted( + getAudioOutputDevice(), + ENABLE_AVB_VOLUME_INFO, + AudioManager.ADJUST_SAME, + AudioDeviceVolumeManager.ADJUST_MODE_NORMAL + ); + mTestLooper.dispatchAll(); + + // Device sends <Give Audio Status> + assertThat(mNativeWrapper.getResultMessages()).contains( + HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(), + getSystemAudioDeviceLogicalAddress())); + + clearInvocations(mAudioManager); + + // Device receives <Report Audio Status> with a new volume and mute state + mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus( + getSystemAudioDeviceLogicalAddress(), + getLogicalAddress(), + 80, + true)); + mTestLooper.dispatchAll(); + + // HdmiControlService calls setStreamVolume and adjustStreamVolume to trigger volume UI + verify(mAudioManager).setStreamVolume( + eq(AudioManager.STREAM_MUSIC), + // Volume level is rescaled to the max volume of STREAM_MUSIC + eq(80 * STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME), + eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI)); + verify(mAudioManager).adjustStreamVolume( + eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_MUTE), + eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI)); + } + + /** + * Tests that a volume adjustment command with direction ADJUST_SAME causes HdmiControlService + * to request the System Audio device's audio status, and notify AudioService of the + * audio status, even if it's unchanged from the previous one. + */ + @Test + public void avbEnabled_audioDeviceVolumeAdjusted_adjustSame_noChange_updatesAudioService() { + enableAbsoluteVolumeBehavior(); + mNativeWrapper.clearResultMessages(); + + // HdmiControlService receives a volume adjustment with direction ADJUST_SAME + mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted( + getAudioOutputDevice(), + ENABLE_AVB_VOLUME_INFO, + AudioManager.ADJUST_SAME, + AudioDeviceVolumeManager.ADJUST_MODE_NORMAL + ); + mTestLooper.dispatchAll(); + + // Device sends <Give Audio Status> + assertThat(mNativeWrapper.getResultMessages()).contains( + HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(), + getSystemAudioDeviceLogicalAddress())); + + clearInvocations(mAudioManager); + + // Device receives <Report Audio Status> with the same volume level and mute state that + // as when AVB was enabled + mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus( + getSystemAudioDeviceLogicalAddress(), + getLogicalAddress(), + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(), + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute())); + mTestLooper.dispatchAll(); + + // HdmiControlService calls setStreamVolume and adjustStreamVolume to trigger volume UI + verify(mAudioManager).setStreamVolume( + eq(AudioManager.STREAM_MUSIC), + // Volume level is rescaled to the max volume of STREAM_MUSIC + eq(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume() + * STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME), + eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI)); + verify(mAudioManager).adjustStreamVolume( + eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_UNMUTE), + eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI)); + } } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index cb659b6dbd56..ecd35a55e291 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -1565,6 +1565,25 @@ public final class UserManagerTest { assertThat(userInfo.name).isEqualTo(newName); } + @Test + public void testCannotCreateAdditionalMainUser() { + UserHandle mainUser = mUserManager.getMainUser(); + assumeTrue("There is no main user", mainUser != null); + + // Users with FLAG_MAIN can't be removed, so no point using the local createUser method. + UserInfo newMainUser = mUserManager.createUser("test", UserInfo.FLAG_MAIN); + assertThat(newMainUser).isNull(); + + List<UserInfo> users = mUserManager.getUsers(); + int mainUserCount = 0; + for (UserInfo user : users) { + if (user.isMain()) { + mainUserCount++; + } + } + assertThat(mainUserCount).isEqualTo(1); + } + private boolean isPackageInstalledForUser(String packageName, int userId) { try { return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index fae92d9ac738..f83a1df358bd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -36,7 +36,7 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -44,7 +44,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.ActivityManager; -import android.app.IActivityManager; import android.app.Notification; import android.app.Notification.Builder; import android.app.NotificationChannel; @@ -76,6 +75,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.server.LocalServices; import com.android.server.UiServiceTestCase; import com.android.server.uri.UriGrantsManagerInternal; @@ -850,84 +850,78 @@ public class NotificationRecordTest extends UiServiceTestCase { @Test public void testCalculateGrantableUris_PappProvided() { - IActivityManager am = mock(IActivityManager.class); UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class); when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class), anyInt(), anyInt())).thenThrow(new SecurityException()); + LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); + LocalServices.addService(UriGrantsManagerInternal.class, ugm); + channel.setSound(null, null); Notification n = new Notification.Builder(mContext, channel.getId()) .setSmallIcon(Icon.createWithContentUri(Uri.parse("content://something"))) .build(); StatusBarNotification sbn = new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid); - NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); - record.mAm = am; - record.mUgmInternal = ugm; - - try { - record.calculateGrantableUris(); - fail("App provided uri for p targeting app should throw exception"); - } catch (SecurityException e) { - // expected - } + + assertThrows("App provided uri for p targeting app should throw exception", + SecurityException.class, + () -> new NotificationRecord(mMockContext, sbn, channel)); } @Test public void testCalculateGrantableUris_PappProvided_invalidSound() { - IActivityManager am = mock(IActivityManager.class); UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class); when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class), anyInt(), anyInt())).thenThrow(new SecurityException()); + LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); + LocalServices.addService(UriGrantsManagerInternal.class, ugm); + channel.setSound(Uri.parse("content://something"), mock(AudioAttributes.class)); Notification n = mock(Notification.class); when(n.getChannelId()).thenReturn(channel.getId()); StatusBarNotification sbn = new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid); - NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); - record.mAm = am; - record.mUgmInternal = ugm; - record.calculateGrantableUris(); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, record.getSound()); } @Test public void testCalculateGrantableUris_PuserOverridden() { - IActivityManager am = mock(IActivityManager.class); UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class); when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class), anyInt(), anyInt())).thenThrow(new SecurityException()); + LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); + LocalServices.addService(UriGrantsManagerInternal.class, ugm); + channel.lockFields(NotificationChannel.USER_LOCKED_SOUND); Notification n = mock(Notification.class); when(n.getChannelId()).thenReturn(channel.getId()); StatusBarNotification sbn = new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid); - NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); - record.mAm = am; - record.calculateGrantableUris(); + new NotificationRecord(mMockContext, sbn, channel); // should not throw } @Test public void testCalculateGrantableUris_prePappProvided() { - IActivityManager am = mock(IActivityManager.class); UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class); when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class), anyInt(), anyInt())).thenThrow(new SecurityException()); + LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); + LocalServices.addService(UriGrantsManagerInternal.class, ugm); + Notification n = mock(Notification.class); when(n.getChannelId()).thenReturn(channel.getId()); StatusBarNotification sbn = new StatusBarNotification(PKG_O, PKG_O, id1, tag1, uid, uid, n, mUser, null, uid); - NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); - record.mAm = am; - record.calculateGrantableUris(); - // should not throw + new NotificationRecord(mMockContext, sbn, channel); // should not throw } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index d5afe3b2f078..0cdd9b8a9e0b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1880,6 +1880,11 @@ public class SizeCompatTests extends WindowTestsBase { final int dh = 2500; final int notchHeight = 200; setUpApp(new TestDisplayContent.Builder(mAtm, dw, dh).setNotch(notchHeight).build()); + // The test assumes the notch will be at left side when the orientation is landscape. + if (mContext.getResources().getBoolean( + com.android.internal.R.bool.config_reverseDefaultRotation)) { + setReverseDefaultRotation(mActivity.mDisplayContent, false); + } addStatusBar(mActivity.mDisplayContent); mActivity.setVisible(false); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ae7b161dea16..99688dabda6e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -48,6 +48,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -59,6 +60,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import android.annotation.IntDef; @@ -956,6 +958,38 @@ class WindowTestsBase extends SystemServiceTestsBase { return testPlayer; } + /** Overrides the behavior of config_reverseDefaultRotation for the given display. */ + void setReverseDefaultRotation(DisplayContent dc, boolean reverse) { + final DisplayRotation displayRotation = dc.getDisplayRotation(); + if (!Mockito.mockingDetails(displayRotation).isSpy()) { + spyOn(displayRotation); + } + doAnswer(invocation -> { + invocation.callRealMethod(); + final int w = invocation.getArgument(0); + final int h = invocation.getArgument(1); + if (w > h) { + if (reverse) { + displayRotation.mPortraitRotation = Surface.ROTATION_90; + displayRotation.mUpsideDownRotation = Surface.ROTATION_270; + } else { + displayRotation.mPortraitRotation = Surface.ROTATION_270; + displayRotation.mUpsideDownRotation = Surface.ROTATION_90; + } + } else { + if (reverse) { + displayRotation.mLandscapeRotation = Surface.ROTATION_270; + displayRotation.mSeascapeRotation = Surface.ROTATION_90; + } else { + displayRotation.mLandscapeRotation = Surface.ROTATION_90; + displayRotation.mSeascapeRotation = Surface.ROTATION_270; + } + } + return null; + }).when(displayRotation).configure(anyInt(), anyInt()); + displayRotation.configure(dc.mBaseDisplayWidth, dc.mBaseDisplayHeight); + } + /** * Avoids rotating screen disturbed by some conditions. It is usually used for the default * display that is not the instance of {@link TestDisplayContent} (it bypasses the conditions). diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 943d8d6cdd55..4a541da9e5aa 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -3388,7 +3388,11 @@ public abstract class Connection extends Conferenceable { public void onAbort() {} /** - * Notifies this Connection of a request to hold. + * Notifies this Connection of a request to hold. {@link Connection#setOnHold} should be within + * the onHold() body in order to transition the call state to {@link Connection#STATE_HOLDING}. + * <p> + * Note: If the Connection does not transition to {@link Connection#STATE_HOLDING} within 2 + * seconds, the call will be disconnected. */ public void onHold() {} diff --git a/telecomm/java/android/telecom/RemoteConnectionManager.java b/telecomm/java/android/telecom/RemoteConnectionManager.java index fbbfefd9d00e..fbf8eeffd947 100644 --- a/telecomm/java/android/telecom/RemoteConnectionManager.java +++ b/telecomm/java/android/telecom/RemoteConnectionManager.java @@ -39,18 +39,21 @@ public class RemoteConnectionManager { void addConnectionService( ComponentName componentName, IConnectionService outgoingConnectionServiceRpc) { - if (!mRemoteConnectionServices.containsKey(componentName)) { - try { - RemoteConnectionService remoteConnectionService = new RemoteConnectionService( - outgoingConnectionServiceRpc, - mOurConnectionServiceImpl); - mRemoteConnectionServices.put(componentName, remoteConnectionService); - } catch (RemoteException e) { - Log.w(RemoteConnectionManager.this, - "error when addConnectionService of %s: %s", componentName, - e.toString()); - } - } + mRemoteConnectionServices.computeIfAbsent( + componentName, + key -> { + try { + return new RemoteConnectionService( + outgoingConnectionServiceRpc, mOurConnectionServiceImpl); + } catch (RemoteException e) { + Log.w( + RemoteConnectionManager.this, + "error when addConnectionService of %s: %s", + componentName, + e.toString()); + return null; + } + }); } public RemoteConnection createRemoteConnection( @@ -63,17 +66,14 @@ public class RemoteConnectionManager { } ComponentName componentName = request.getAccountHandle().getComponentName(); - if (!mRemoteConnectionServices.containsKey(componentName)) { + RemoteConnectionService remoteService = mRemoteConnectionServices.get(componentName); + if (remoteService == null) { throw new UnsupportedOperationException("accountHandle not supported: " + componentName); } - RemoteConnectionService remoteService = mRemoteConnectionServices.get(componentName); - if (remoteService != null) { - return remoteService.createRemoteConnection( - connectionManagerPhoneAccount, request, isIncoming); - } - return null; + return remoteService.createRemoteConnection( + connectionManagerPhoneAccount, request, isIncoming); } /** @@ -94,17 +94,14 @@ public class RemoteConnectionManager { } ComponentName componentName = request.getAccountHandle().getComponentName(); - if (!mRemoteConnectionServices.containsKey(componentName)) { + RemoteConnectionService remoteService = mRemoteConnectionServices.get(componentName); + if (remoteService == null) { throw new UnsupportedOperationException("accountHandle not supported: " + componentName); } - RemoteConnectionService remoteService = mRemoteConnectionServices.get(componentName); - if (remoteService != null) { - return remoteService.createRemoteConference( - connectionManagerPhoneAccount, request, isIncoming); - } - return null; + return remoteService.createRemoteConference( + connectionManagerPhoneAccount, request, isIncoming); } public void conferenceRemoteConnections(RemoteConnection a, RemoteConnection b) { diff --git a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java new file mode 100644 index 000000000000..d8113fc95591 --- /dev/null +++ b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.ViewConfiguration; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest FocusEventDebugViewTest + */ +@RunWith(AndroidJUnit4.class) +public class FocusEventDebugViewTest { + + private FocusEventDebugView mFocusEventDebugView; + private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView; + private float mScaledVerticalScrollFactor; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + mScaledVerticalScrollFactor = + ViewConfiguration.get(context).getScaledVerticalScrollFactor(); + InputManagerService mockService = mock(InputManagerService.class); + when(mockService.monitorInput(anyString(), anyInt())) + .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]); + + mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context); + mFocusEventDebugView = new FocusEventDebugView(context, mockService, + () -> mRotaryInputValueView); + } + + @Test + public void startsRotaryInputValueViewWithDefaultValue() { + assertEquals("+0.0", mRotaryInputValueView.getText()); + } + + @Test + public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() { + mFocusEventDebugView.handleUpdateShowRotaryInput(true); + + mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f)); + + assertEquals(String.format("+%.1f", 0.5f * mScaledVerticalScrollFactor), + mRotaryInputValueView.getText()); + } + + @Test + public void updateActivityStatus_setsAndRemovesColorFilter() { + // It should not be active initially. + assertNull(mRotaryInputValueView.getBackground().getColorFilter()); + + mRotaryInputValueView.updateActivityStatus(true); + // It should be active after rotary input. + assertNotNull(mRotaryInputValueView.getBackground().getColorFilter()); + + mRotaryInputValueView.updateActivityStatus(false); + // It should not be active after waiting for mUpdateActivityStatusCallback. + assertNull(mRotaryInputValueView.getBackground().getColorFilter()); + } + + private MotionEvent createRotaryMotionEvent(float scrollAxisValue) { + PointerCoords pointerCoords = new PointerCoords(); + pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue); + PointerProperties pointerProperties = new PointerProperties(); + + return MotionEvent.obtain( + /* downTime */ 0, + /* eventTime */ 0, + /* action */ MotionEvent.ACTION_SCROLL, + /* pointerCount */ 1, + /* pointerProperties */ new PointerProperties[] {pointerProperties}, + /* pointerCoords */ new PointerCoords[] {pointerCoords}, + /* metaState */ 0, + /* buttonState */ 0, + /* xPrecision */ 0, + /* yPrecision */ 0, + /* deviceId */ 0, + /* edgeFlags */ 0, + /* source */ InputDevice.SOURCE_ROTARY_ENCODER, + /* flags */ 0 + ); + } +} diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt index 31ea8327c2b3..c92d768439fd 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt @@ -16,10 +16,6 @@ package com.android.test.silkfx.hdr -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.animation.ValueAnimator -import android.animation.ValueAnimator.AnimatorUpdateListener import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas @@ -46,7 +42,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context private var selectedImage = -1 private var outputMode = R.id.output_hdr private var bitmap: Bitmap? = null - private var gainmap: Gainmap? = null + private var originalGainmap: Gainmap? = null private var gainmapVisualizer: Bitmap? = null private lateinit var imageView: SubsamplingScaleImageView private lateinit var gainmapMetadataEditor: GainmapMetadataEditor @@ -70,7 +66,6 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context it.check(outputMode) it.setOnCheckedChangeListener { _, checkedId -> outputMode = checkedId - // Intentionally don't do anything fancy so that mode A/B comparisons are easy updateDisplay() } } @@ -101,41 +96,10 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context imageView.apply { isClickable = true - // Example of animating between SDR and HDR using gainmap params; animates HDR->SDR->HDR - // with a brief pause on SDR. The key thing here is that the gainmap's - // minDisplayRatioForHdrTransition is animated between its original value (for full HDR) - // and displayRatioForFullHdr (for full SDR). The view must also be invalidated during - // the animation for the updates to take effect. setOnClickListener { - if (gainmap != null && (outputMode == R.id.output_hdr || - outputMode == R.id.output_hdr_test)) { - val animationLengthMs: Long = 500 - val updateListener = object : AnimatorUpdateListener { - override fun onAnimationUpdate(animation: ValueAnimator) { - imageView.invalidate() - } - } - val hdrToSdr = ObjectAnimator.ofFloat( - gainmap, "minDisplayRatioForHdrTransition", - gainmap!!.minDisplayRatioForHdrTransition, - gainmap!!.displayRatioForFullHdr).apply { - duration = animationLengthMs - addUpdateListener(updateListener) - } - val sdrToHdr = ObjectAnimator.ofFloat( - gainmap, "minDisplayRatioForHdrTransition", - gainmap!!.displayRatioForFullHdr, - gainmap!!.minDisplayRatioForHdrTransition).apply { - duration = animationLengthMs - addUpdateListener(updateListener) - } - - AnimatorSet().apply { - play(hdrToSdr) - play(sdrToHdr).after(animationLengthMs) - start() - } - } + animate().alpha(.5f).withEndAction { + animate().alpha(1f).start() + }.start() } } } @@ -149,7 +113,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context } private fun doDecode(source: ImageDecoder.Source) { - gainmap = null + originalGainmap = null bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } @@ -167,9 +131,10 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context findViewById<TextView>(R.id.error_msg)!!.visibility = View.GONE findViewById<RadioGroup>(R.id.output_mode)!!.visibility = View.VISIBLE - gainmap = bitmap!!.gainmap - gainmapMetadataEditor.setGainmap(gainmap) - val map = gainmap!!.gainmapContents + val gainmap = bitmap!!.gainmap!! + originalGainmap = gainmap + gainmapMetadataEditor.setGainmap(Gainmap(gainmap, gainmap.gainmapContents)) + val map = gainmap.gainmapContents if (map.config != Bitmap.Config.ALPHA_8) { gainmapVisualizer = map } else { @@ -198,14 +163,12 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context imageView.setImage(ImageSource.cachedBitmap(when (outputMode) { R.id.output_hdr -> { - gainmapMetadataEditor.useOriginalMetadata() - bitmap!!.gainmap = gainmap + bitmap!!.gainmap = originalGainmap bitmap!! } R.id.output_hdr_test -> { - gainmapMetadataEditor.useEditMetadata() - bitmap!!.gainmap = gainmap + bitmap!!.gainmap = gainmapMetadataEditor.editedGainmap() bitmap!! } diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt index 8a653045c97b..c4bc6001533e 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt @@ -28,23 +28,27 @@ import android.widget.TextView import com.android.test.silkfx.R data class GainmapMetadata( - var ratioMin: Float, - var ratioMax: Float, - var capacityMin: Float, - var capacityMax: Float, - var gamma: Float, - var offsetSdr: Float, - var offsetHdr: Float + var ratioMin: Float, + var ratioMax: Float, + var capacityMin: Float, + var capacityMax: Float, + var gamma: Float, + var offsetSdr: Float, + var offsetHdr: Float ) +/** + * Note: This can only handle single-channel gainmaps nicely. It will force all 3-channel + * metadata to have the same value single value and is not intended to be a robust demonstration + * of gainmap metadata editing + */ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { - private var gainmap: Gainmap? = null - private var showingEdits = false + private lateinit var gainmap: Gainmap private var metadataPopup: PopupWindow? = null private var originalMetadata: GainmapMetadata = GainmapMetadata( - 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f) + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f) private var currentMetadata: GainmapMetadata = originalMetadata.copy() private val maxProgress = 100.0f @@ -61,23 +65,18 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { private val maxGamma = 3.0f // Min and max offsets are 0.0 and 1.0 respectively - fun setGainmap(newGainmap: Gainmap?) { + fun setGainmap(newGainmap: Gainmap) { gainmap = newGainmap - originalMetadata = GainmapMetadata(gainmap!!.getRatioMin()[0], - gainmap!!.getRatioMax()[0], gainmap!!.getMinDisplayRatioForHdrTransition(), - gainmap!!.getDisplayRatioForFullHdr(), gainmap!!.getGamma()[0], - gainmap!!.getEpsilonSdr()[0], gainmap!!.getEpsilonHdr()[0]) + originalMetadata = GainmapMetadata(gainmap.getRatioMin()[0], + gainmap.getRatioMax()[0], gainmap.getMinDisplayRatioForHdrTransition(), + gainmap.getDisplayRatioForFullHdr(), gainmap.getGamma()[0], + gainmap.getEpsilonSdr()[0], gainmap.getEpsilonHdr()[0]) currentMetadata = originalMetadata.copy() } - fun useOriginalMetadata() { - showingEdits = false - applyMetadata(originalMetadata) - } - - fun useEditMetadata() { - showingEdits = true + fun editedGainmap(): Gainmap { applyMetadata(currentMetadata) + return gainmap } fun closeEditor() { @@ -93,7 +92,7 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val view = LayoutInflater.from(parent.getContext()).inflate(R.layout.gainmap_metadata, null) metadataPopup = PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT) + ViewGroup.LayoutParams.WRAP_CONTENT) metadataPopup!!.showAtLocation(view, Gravity.CENTER, 0, 0) (view.getParent() as ViewGroup).removeView(view) @@ -117,7 +116,7 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val offsetSdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsetsdr) val offsetHdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) arrayOf(gainmapMinSeek, gainmapMaxSeek, capacityMinSeek, capacityMaxSeek, gammaSeek, - offsetSdrSeek, offsetHdrSeek).forEach { + offsetSdrSeek, offsetHdrSeek).forEach { it.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (!fromUser) return @@ -149,37 +148,37 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val offsetHdrSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) gainmapMinSeek.setProgress( - ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt()) + ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt()) gainmapMaxSeek.setProgress( - ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt()) + ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt()) capacityMinSeek.setProgress( - ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt()) + ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt()) capacityMaxSeek.setProgress( - ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt()) + ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt()) gammaSeek.setProgress( - ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt()) + ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt()) // Log base 3 via: log_b(x) = log_y(x) / log_y(b) offsetSdrSeek.setProgress( - ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0) - .toFloat() * maxProgress).toInt()) + ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) offsetHdrSeek.setProgress( - ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0) - .toFloat() * maxProgress).toInt()) + ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText( - "%.3f".format(currentMetadata.ratioMin)) + "%.3f".format(currentMetadata.ratioMin)) parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText( - "%.3f".format(currentMetadata.ratioMax)) + "%.3f".format(currentMetadata.ratioMax)) parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText( - "%.3f".format(currentMetadata.capacityMin)) + "%.3f".format(currentMetadata.capacityMin)) parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText( - "%.3f".format(currentMetadata.capacityMax)) + "%.3f".format(currentMetadata.capacityMax)) parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText( - "%.3f".format(currentMetadata.gamma)) + "%.3f".format(currentMetadata.gamma)) parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText( - "%.5f".format(currentMetadata.offsetSdr)) + "%.5f".format(currentMetadata.offsetSdr)) parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText( - "%.5f".format(currentMetadata.offsetHdr)) + "%.5f".format(currentMetadata.offsetHdr)) } private fun resetGainmapMetadata() { @@ -189,69 +188,59 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { } private fun applyMetadata(newMetadata: GainmapMetadata) { - gainmap!!.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin) - gainmap!!.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax) - gainmap!!.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin) - gainmap!!.setDisplayRatioForFullHdr(newMetadata.capacityMax) - gainmap!!.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma) - gainmap!!.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr) - gainmap!!.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr) + gainmap.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin) + gainmap.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax) + gainmap.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin) + gainmap.setDisplayRatioForFullHdr(newMetadata.capacityMax) + gainmap.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma) + gainmap.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr) + gainmap.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr) renderView.invalidate() } private fun updateGainmapMin(normalized: Float) { val newValue = minRatioMin + normalized * (maxRatioMin - minRatioMin) parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.ratioMin = newValue - if (showingEdits) { - gainmap!!.setRatioMin(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setRatioMin(newValue, newValue, newValue) + renderView.invalidate() } private fun updateGainmapMax(normalized: Float) { val newValue = minRatioMax + normalized * (maxRatioMax - minRatioMax) parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.ratioMax = newValue - if (showingEdits) { - gainmap!!.setRatioMax(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setRatioMax(newValue, newValue, newValue) + renderView.invalidate() } private fun updateCapacityMin(normalized: Float) { val newValue = minCapacityMin + normalized * (maxCapacityMin - minCapacityMin) parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.capacityMin = newValue - if (showingEdits) { - gainmap!!.setMinDisplayRatioForHdrTransition(newValue) - renderView.invalidate() - } + gainmap.setMinDisplayRatioForHdrTransition(newValue) + renderView.invalidate() } private fun updateCapacityMax(normalized: Float) { val newValue = minCapacityMax + normalized * (maxCapacityMax - minCapacityMax) parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.capacityMax = newValue - if (showingEdits) { - gainmap!!.setDisplayRatioForFullHdr(newValue) - renderView.invalidate() - } + gainmap.setDisplayRatioForFullHdr(newValue) + renderView.invalidate() } private fun updateGamma(normalized: Float) { val newValue = minGamma + normalized * (maxGamma - minGamma) parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.gamma = newValue - if (showingEdits) { - gainmap!!.setGamma(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setGamma(newValue, newValue, newValue) + renderView.invalidate() } private fun updateOffsetSdr(normalized: Float) { @@ -260,12 +249,10 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() } parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText( - "%.5f".format(newValue)) + "%.5f".format(newValue)) currentMetadata.offsetSdr = newValue - if (showingEdits) { - gainmap!!.setEpsilonSdr(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setEpsilonSdr(newValue, newValue, newValue) + renderView.invalidate() } private fun updateOffsetHdr(normalized: Float) { @@ -274,11 +261,9 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() } parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText( - "%.5f".format(newValue)) + "%.5f".format(newValue)) currentMetadata.offsetHdr = newValue - if (showingEdits) { - gainmap!!.setEpsilonHdr(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setEpsilonHdr(newValue, newValue, newValue) + renderView.invalidate() } } diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp index d02fd83df6af..51cf38bce64e 100644 --- a/tools/aapt/Command.cpp +++ b/tools/aapt/Command.cpp @@ -353,8 +353,8 @@ static void printCompatibleScreens(ResXMLTree& tree, String8* outError) { } static void printUsesPermission(const String8& name, bool optional=false, int maxSdkVersion=-1, - const String8& requiredFeature = String8::empty(), - const String8& requiredNotFeature = String8::empty()) { + const String8& requiredFeature = String8(), + const String8& requiredNotFeature = String8()) { printf("uses-permission: name='%s'", ResTable::normalizeForOutput(name.string()).string()); if (maxSdkVersion != -1) { printf(" maxSdkVersion='%d'", maxSdkVersion); diff --git a/tools/hiddenapi/OWNERS b/tools/hiddenapi/OWNERS index afbeef5a0b41..dc82aac9d41c 100644 --- a/tools/hiddenapi/OWNERS +++ b/tools/hiddenapi/OWNERS @@ -1,5 +1,4 @@ # compat-team@ for changes to hiddenapi files -andreionea@google.com mathewi@google.com satayev@google.com diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java index 5ad3ede8498d..c828de9f221d 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java @@ -196,7 +196,7 @@ public final class SharedConnectivitySettingsState implements Parcelable { public String toString() { return new StringBuilder("SharedConnectivitySettingsState[") .append("instantTetherEnabled=").append(mInstantTetherEnabled) - .append("PendingIntent=").append(mInstantTetherSettingsPendingIntent.toString()) + .append("PendingIntent=").append(mInstantTetherSettingsPendingIntent) .append("extras=").append(mExtras.toString()) .append("]").toString(); } |