diff options
Diffstat (limited to 'packages/SystemUI/src')
189 files changed, 5614 insertions, 1914 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java index 08076c17dc3a..fcf4e4703ed3 100644 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java @@ -94,7 +94,9 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie @Override public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { if (mKeyguardShowing && !mIsCharging && charging) { - mView.animateCharge(mIsDozing); + mView.animateCharge(() -> { + return mStatusBarStateController.isDozing(); + }); } mIsCharging = charging; } @@ -210,6 +212,7 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie } else { mView.setLineSpacingScale(mDefaultLineSpacing); } + mView.refreshFormat(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java index 63867c0f7308..ef3104a21708 100644 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java @@ -19,9 +19,9 @@ package com.android.keyguard; import android.annotation.FloatRange; import android.annotation.IntRange; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.icu.text.DateTimePatternGenerator; import android.text.format.DateFormat; import android.util.AttributeSet; import android.widget.TextView; @@ -30,6 +30,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.phone.KeyguardBypassController; import java.util.Calendar; +import java.util.Locale; import java.util.TimeZone; import kotlin.Unit; @@ -41,8 +42,6 @@ import kotlin.Unit; public class AnimatableClockView extends TextView { private static final CharSequence DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"; private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"; - private static final CharSequence SINGLE_LINE_FORMAT_12_HOUR = "h:mm"; - private static final CharSequence SINGLE_LINE_FORMAT_24_HOUR = "HH:mm"; private static final long DOZE_ANIM_DURATION = 300; private static final long APPEAR_ANIM_DURATION = 350; private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500; @@ -196,20 +195,20 @@ public class AnimatableClockView extends TextView { null /* onAnimationEnd */); } - void animateCharge(boolean isDozing) { + void animateCharge(DozeStateGetter dozeStateGetter) { if (mTextAnimator == null || mTextAnimator.isRunning()) { // Skip charge animation if dozing animation is already playing. return; } Runnable startAnimPhase2 = () -> setTextStyle( - isDozing ? mDozingWeight : mLockScreenWeight/* weight */, + dozeStateGetter.isDozing() ? mDozingWeight : mLockScreenWeight/* weight */, -1, null, true /* animate */, CHARGE_ANIM_DURATION_PHASE_1, 0 /* delay */, null /* onAnimationEnd */); - setTextStyle(isDozing ? mLockScreenWeight : mDozingWeight/* weight */, + setTextStyle(dozeStateGetter.isDozing() ? mLockScreenWeight : mDozingWeight/* weight */, -1, null, true /* animate */, @@ -259,24 +258,50 @@ public class AnimatableClockView extends TextView { } void refreshFormat() { + Patterns.update(mContext); + final boolean use24HourFormat = DateFormat.is24HourFormat(getContext()); if (mIsSingleLine && use24HourFormat) { - mFormat = SINGLE_LINE_FORMAT_24_HOUR; + mFormat = Patterns.sClockView24; } else if (!mIsSingleLine && use24HourFormat) { mFormat = DOUBLE_LINE_FORMAT_24_HOUR; } else if (mIsSingleLine && !use24HourFormat) { - mFormat = SINGLE_LINE_FORMAT_12_HOUR; + mFormat = Patterns.sClockView12; } else { mFormat = DOUBLE_LINE_FORMAT_12_HOUR; } - mDescFormat = getBestDateTimePattern(getContext(), use24HourFormat ? "Hm" : "hm"); + mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12; refreshTime(); } - private static String getBestDateTimePattern(Context context, String skeleton) { - DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance( - context.getResources().getConfiguration().locale); - return dtpg.getBestPattern(skeleton); + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. + // This is an optimization to ensure we only recompute the patterns when the inputs change. + private static final class Patterns { + static String sClockView12; + static String sClockView24; + static String sCacheKey; + + static void update(Context context) { + final Locale locale = Locale.getDefault(); + final Resources res = context.getResources(); + final String clockView12Skel = res.getString(R.string.clock_12hr_format); + final String clockView24Skel = res.getString(R.string.clock_24hr_format); + final String key = locale.toString() + clockView12Skel + clockView24Skel; + if (key.equals(sCacheKey)) return; + sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); + + // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton + // format. The following code removes the AM/PM indicator if we didn't want it. + if (!clockView12Skel.contains("a")) { + sClockView12 = sClockView12.replaceAll("a", "").trim(); + } + sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); + sCacheKey = key; + } + } + + interface DozeStateGetter { + boolean isDozing(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java index 920cf003a893..7064b8e22f2a 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java @@ -37,6 +37,7 @@ import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.dagger.KeyguardBouncerScope; +import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.util.EmergencyDialerConstants; @@ -54,6 +55,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { private final TelephonyManager mTelephonyManager; private final PowerManager mPowerManager; private final ActivityTaskManager mActivityTaskManager; + private ShadeController mShadeController; private final TelecomManager mTelecomManager; private final MetricsLogger mMetricsLogger; @@ -94,6 +96,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { ConfigurationController configurationController, KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager, PowerManager powerManager, ActivityTaskManager activityTaskManager, + ShadeController shadeController, @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger) { super(view); mConfigurationController = configurationController; @@ -101,6 +104,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { mTelephonyManager = telephonyManager; mPowerManager = powerManager; mActivityTaskManager = activityTaskManager; + mShadeController = shadeController; mTelecomManager = telecomManager; mMetricsLogger = metricsLogger; } @@ -145,6 +149,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { mPowerManager.userActivity(SystemClock.uptimeMillis(), true); } mActivityTaskManager.stopSystemLockTaskMode(); + mShadeController.collapsePanel(false); if (mTelecomManager != null && mTelecomManager.isInCall()) { mTelecomManager.showInCallScreen(false); if (mEmergencyButtonCallback != null) { @@ -214,6 +219,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { private final TelephonyManager mTelephonyManager; private final PowerManager mPowerManager; private final ActivityTaskManager mActivityTaskManager; + private ShadeController mShadeController; @Nullable private final TelecomManager mTelecomManager; private final MetricsLogger mMetricsLogger; @@ -222,6 +228,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { public Factory(ConfigurationController configurationController, KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager, PowerManager powerManager, ActivityTaskManager activityTaskManager, + ShadeController shadeController, @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger) { mConfigurationController = configurationController; @@ -229,6 +236,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { mTelephonyManager = telephonyManager; mPowerManager = powerManager; mActivityTaskManager = activityTaskManager; + mShadeController = shadeController; mTelecomManager = telecomManager; mMetricsLogger = metricsLogger; } @@ -237,6 +245,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { public EmergencyButtonController create(EmergencyButton view) { return new EmergencyButtonController(view, mConfigurationController, mKeyguardUpdateMonitor, mTelephonyManager, mPowerManager, mActivityTaskManager, + mShadeController, mTelecomManager, mMetricsLogger); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index ef052c4b81ac..a5b25097a56e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -57,6 +57,7 @@ public class KeyguardClockSwitch extends RelativeLayout { private View mKeyguardStatusArea; /** Mutually exclusive with mKeyguardStatusArea */ private View mSmartspaceView; + private int mSmartspaceTopOffset; /** * Maintain state so that a newly connected plugin can be initialized. @@ -96,6 +97,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_clock_switch_y_shift); + + mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_smartspace_top_offset); } /** @@ -193,7 +197,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (indexOfChild(in) == -1) addView(in); direction = -1; smartspaceYTranslation = mSmartspaceView == null ? 0 - : mClockFrame.getTop() - mSmartspaceView.getTop(); + : mClockFrame.getTop() - mSmartspaceView.getTop() + mSmartspaceTopOffset; } else { in = mClockFrame; out = mLargeClockFrame; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index a28d1747f2fe..632919ae51e4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -20,9 +20,7 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.WallpaperManager; -import android.content.res.Resources; import android.text.TextUtils; -import android.text.format.DateFormat; import android.view.View; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -189,10 +187,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS .getDimensionPixelSize(R.dimen.below_clock_padding_end); mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0); - // ... but above the large clock - lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); - lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId()); - mLargeClockFrame.setLayoutParams(lp); + updateClockLayout(); View nic = mView.findViewById( R.id.left_aligned_notification_icon_container); @@ -235,6 +230,18 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS */ public void onDensityOrFontScaleChanged() { mView.onDensityOrFontScaleChanged(); + + updateClockLayout(); + } + + private void updateClockLayout() { + if (mSmartspaceController.isEnabled()) { + RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, + MATCH_PARENT); + lp.topMargin = getContext().getResources().getDimensionPixelSize( + R.dimen.keyguard_large_clock_top_margin); + mLargeClockFrame.setLayoutParams(lp); + } } /** @@ -358,37 +365,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return mColorExtractor.getColors(WallpaperManager.FLAG_LOCK); } - // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. - // This is an optimization to ensure we only recompute the patterns when the inputs change. - private static final class Patterns { - static String sClockView12; - static String sClockView24; - static String sCacheKey; - - static void update(Resources res) { - final Locale locale = Locale.getDefault(); - final String clockView12Skel = res.getString(R.string.clock_12hr_format); - final String clockView24Skel = res.getString(R.string.clock_24hr_format); - final String key = locale.toString() + clockView12Skel + clockView24Skel; - if (key.equals(sCacheKey)) return; - - sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); - // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton - // format. The following code removes the AM/PM indicator if we didn't want it. - if (!clockView12Skel.contains("a")) { - sClockView12 = sClockView12.replaceAll("a", "").trim(); - } - - sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); - - // Use fancy colon. - sClockView24 = sClockView24.replace(':', '\uee01'); - sClockView12 = sClockView12.replace(':', '\uee01'); - - sCacheKey = key; - } - } - private int getCurrentLayoutDirection() { return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java index 568bea0e2d24..62411dbff5fd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java @@ -54,6 +54,7 @@ public class KeyguardMessageArea extends TextView implements SecurityMessageDisp private CharSequence mMessage; private ColorStateList mNextMessageColorState = ColorStateList.valueOf(DEFAULT_COLOR); private boolean mBouncerVisible; + private boolean mAltBouncerShowing; public KeyguardMessageArea(Context context, AttributeSet attrs) { super(context, attrs); @@ -144,7 +145,8 @@ public class KeyguardMessageArea extends TextView implements SecurityMessageDisp void update() { CharSequence status = mMessage; - setVisibility(TextUtils.isEmpty(status) || !mBouncerVisible ? INVISIBLE : VISIBLE); + setVisibility(TextUtils.isEmpty(status) || (!mBouncerVisible && !mAltBouncerShowing) + ? INVISIBLE : VISIBLE); setText(status); ColorStateList colorState = mDefaultColorState; if (mNextMessageColorState.getDefaultColor() != DEFAULT_COLOR) { @@ -159,6 +161,16 @@ public class KeyguardMessageArea extends TextView implements SecurityMessageDisp } /** + * Set whether the alt bouncer is showing + */ + void setAltBouncerShowing(boolean showing) { + if (mAltBouncerShowing != showing) { + mAltBouncerShowing = showing; + update(); + } + } + + /** * Runnable used to delay accessibility announcements. */ private static class AnnounceRunnable implements Runnable { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index 6e40f025da50..51ded3fcafdf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -28,7 +28,7 @@ import javax.inject.Inject; public class KeyguardMessageAreaController extends ViewController<KeyguardMessageArea> { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; - + private boolean mAltBouncerShowing; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { public void onFinishedGoingToSleep(int why) { @@ -81,6 +81,13 @@ public class KeyguardMessageAreaController extends ViewController<KeyguardMessag mKeyguardUpdateMonitor.removeCallback(mInfoCallback); } + /** + * Set whether alt bouncer is showing + */ + public void setAltBouncerShowing(boolean showing) { + mView.setAltBouncerShowing(showing); + } + public void setMessage(CharSequence s) { mView.setMessage(s); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 2325b554d507..8fc4240e1054 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -17,16 +17,20 @@ package com.android.keyguard; import android.content.Context; +import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; +import android.widget.LinearLayout; import com.android.internal.jank.InteractionJankMonitor; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; import com.android.systemui.R; +import java.util.List; + /** * Displays a PIN pad for unlocking. */ @@ -64,6 +68,11 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { } @Override + protected void onConfigurationChanged(Configuration newConfig) { + updateMargins(); + } + + @Override protected void resetState() { } @@ -72,6 +81,33 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { return R.id.pinEntry; } + private void updateMargins() { + int bottomMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.num_pad_row_margin_bottom); + + for (ViewGroup vg : List.of(mRow1, mRow2, mRow3)) { + ((LinearLayout.LayoutParams) vg.getLayoutParams()).setMargins(0, 0, 0, bottomMargin); + } + + bottomMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.num_pad_entry_row_margin_bottom); + ((LinearLayout.LayoutParams) mRow0.getLayoutParams()).setMargins(0, 0, 0, bottomMargin); + + if (mEcaView != null) { + int ecaTopMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_eca_top_margin); + int ecaBottomMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_eca_bottom_margin); + ((LinearLayout.LayoutParams) mEcaView.getLayoutParams()).setMargins(0, ecaTopMargin, + 0, ecaBottomMargin); + } + + View entryView = findViewById(R.id.pinEntry); + ViewGroup.LayoutParams lp = entryView.getLayoutParams(); + lp.height = mContext.getResources().getDimensionPixelSize(R.dimen.keyguard_password_height); + entryView.setLayoutParams(lp); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 4827cab3b5c0..fde8213de0c6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -173,7 +173,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onSwipeUp() { if (!mUpdateMonitor.isFaceDetectionRunning()) { - mUpdateMonitor.requestFaceAuth(); + mUpdateMonitor.requestFaceAuth(true); mKeyguardSecurityCallback.userActivity(); showMessage(null, null); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 7837f61dc289..0503b2e80d86 100755 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -71,6 +71,7 @@ import android.os.ServiceManager; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.os.Vibrator; import android.provider.Settings; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; @@ -85,6 +86,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import com.android.internal.annotations.VisibleForTesting; @@ -95,6 +97,7 @@ import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.UdfpsController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -283,6 +286,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @VisibleForTesting protected boolean mTelephonyCapable; + private final boolean mAcquiredHapticEnabled; + @Nullable private final Vibrator mVibrator; + // Device provisioning state private boolean mDeviceProvisioned; @@ -310,6 +316,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private RingerModeTracker mRingerModeTracker; private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; private int mFaceRunningState = BIOMETRIC_STATE_STOPPED; + private boolean mIsFaceAuthUserRequested; private LockPatternUtils mLockPatternUtils; private final IDreamManager mDreamManager; private boolean mIsDreaming; @@ -779,8 +786,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) { - mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, - getCurrentUser()); + mFingerprintLockedOutPermanent = true; + requireStrongAuthIfAllLockedOut(); } if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT @@ -801,6 +808,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void handleFingerprintLockoutReset() { mFingerprintLockedOut = false; + mFingerprintLockedOutPermanent = false; updateFingerprintListeningState(); } @@ -961,8 +969,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) { - mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, - getCurrentUser()); + mFaceLockedOutPermanent = true; + requireStrongAuthIfAllLockedOut(); } for (int i = 0; i < mCallbacks.size(); i++) { @@ -975,6 +983,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void handleFaceLockoutReset() { + mFaceLockedOutPermanent = false; updateFaceListeningState(); } @@ -1050,6 +1059,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || isSimPinSecure()); } + private void requireStrongAuthIfAllLockedOut() { + final boolean faceLock = + mFaceLockedOutPermanent || !shouldListenForFace(); + final boolean fpLock = + mFingerprintLockedOutPermanent || !shouldListenForFingerprint(isUdfpsEnrolled()); + + if (faceLock && fpLock) { + Log.d(TAG, "All biometrics locked out - requiring strong auth"); + mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, + getCurrentUser()); + } + } public boolean getUserCanSkipBouncer(int userId) { return getUserHasTrust(userId) || getUserUnlockedWithBiometric(userId); @@ -1333,46 +1354,74 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab handleFingerprintAuthenticated(userId, isStrongBiometric); }; - private final FingerprintManager.AuthenticationCallback mFingerprintAuthenticationCallback + @VisibleForTesting + final FingerprintManager.AuthenticationCallback mFingerprintAuthenticationCallback = new AuthenticationCallback() { + private boolean mPlayedAcquiredHaptic; - @Override - public void onAuthenticationFailed() { - handleFingerprintAuthFailed(); - } + @Override + public void onAuthenticationFailed() { + handleFingerprintAuthFailed(); + } - @Override - public void onAuthenticationSucceeded(AuthenticationResult result) { - Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded"); - handleFingerprintAuthenticated(result.getUserId(), result.isStrongBiometric()); - Trace.endSection(); - } + @Override + public void onAuthenticationSucceeded(AuthenticationResult result) { + Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded"); + handleFingerprintAuthenticated(result.getUserId(), result.isStrongBiometric()); + Trace.endSection(); + + // on auth success, we sometimes never received an acquired haptic + if (!mPlayedAcquiredHaptic && isUdfpsEnrolled()) { + playAcquiredHaptic(); + } + } - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - handleFingerprintHelp(helpMsgId, helpString.toString()); - } + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + handleFingerprintHelp(helpMsgId, helpString.toString()); + } - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - handleFingerprintError(errMsgId, errString.toString()); - } + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + handleFingerprintError(errMsgId, errString.toString()); + } - @Override - public void onAuthenticationAcquired(int acquireInfo) { - handleFingerprintAcquired(acquireInfo); - } + @Override + public void onAuthenticationAcquired(int acquireInfo) { + handleFingerprintAcquired(acquireInfo); + if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD + && isUdfpsEnrolled()) { + mPlayedAcquiredHaptic = true; + playAcquiredHaptic(); + } + } - @Override - public void onUdfpsPointerDown(int sensorId) { - Log.d(TAG, "onUdfpsPointerDown, sensorId: " + sensorId); - } + @Override + public void onUdfpsPointerDown(int sensorId) { + Log.d(TAG, "onUdfpsPointerDown, sensorId: " + sensorId); + mPlayedAcquiredHaptic = false; + } - @Override - public void onUdfpsPointerUp(int sensorId) { - Log.d(TAG, "onUdfpsPointerUp, sensorId: " + sensorId); + @Override + public void onUdfpsPointerUp(int sensorId) { + Log.d(TAG, "onUdfpsPointerUp, sensorId: " + sensorId); + } + }; + + /** + * Play haptic to signal udfps fingeprrint acquired. + */ + @VisibleForTesting + public void playAcquiredHaptic() { + if (mAcquiredHapticEnabled && mVibrator != null) { + String effect = Settings.Global.getString( + mContext.getContentResolver(), + "udfps_acquired_type"); + mVibrator.vibrate(UdfpsController.getVibration(effect, + UdfpsController.EFFECT_TICK), + UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES); } - }; + } private final FaceManager.FaceDetectionCallback mFaceDetectionCallback = (sensorId, userId, isStrongBiometric) -> { @@ -1381,7 +1430,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; @VisibleForTesting - FaceManager.AuthenticationCallback mFaceAuthenticationCallback + final FaceManager.AuthenticationCallback mFaceAuthenticationCallback = new FaceManager.AuthenticationCallback() { @Override @@ -1418,6 +1467,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private FaceManager mFaceManager; private List<FaceSensorPropertiesInternal> mFaceSensorProperties; private boolean mFingerprintLockedOut; + private boolean mFingerprintLockedOutPermanent; + private boolean mFaceLockedOutPermanent; private TelephonyManager mTelephonyManager; /** @@ -1665,7 +1716,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab LockPatternUtils lockPatternUtils, AuthController authController, TelephonyListenerManager telephonyListenerManager, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + @Nullable Vibrator vibrator) { mContext = context; mSubscriptionManager = SubscriptionManager.from(context); mTelephonyListenerManager = telephonyListenerManager; @@ -1680,6 +1732,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLockPatternUtils = lockPatternUtils; mAuthController = authController; dumpManager.registerDumpable(getClass().getName(), this); + mAcquiredHapticEnabled = Settings.Global.getInt(mContext.getContentResolver(), + "udfps_acquired", 0) == 1; + mVibrator = vibrator; mHandler = new Handler(mainLooper) { @Override @@ -2059,12 +2114,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Requests face authentication if we're on a state where it's allowed. * This will re-trigger auth in case it fails. + * @param userInitiatedRequest true if the user explicitly requested face auth */ - public void requestFaceAuth() { - if (DEBUG) Log.d(TAG, "requestFaceAuth()"); + public void requestFaceAuth(boolean userInitiatedRequest) { + if (DEBUG) Log.d(TAG, "requestFaceAuth() userInitiated=" + userInitiatedRequest); + mIsFaceAuthUserRequested |= userInitiatedRequest; updateFaceListeningState(); } + public boolean isFaceAuthUserRequested() { + return mIsFaceAuthUserRequested; + } + /** * In case face auth is running, cancel it. */ @@ -2081,6 +2142,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mHandler.removeCallbacks(mRetryFaceAuthentication); boolean shouldListenForFace = shouldListenForFace(); if (mFaceRunningState == BIOMETRIC_STATE_RUNNING && !shouldListenForFace) { + mIsFaceAuthUserRequested = false; stopListeningForFace(); } else if (mFaceRunningState != BIOMETRIC_STATE_RUNNING && shouldListenForFace) { startListeningForFace(); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java index 423bd5626da9..c1d448db1e63 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.annotation.NonNull; import android.content.Context; import android.graphics.PointF; import android.graphics.RectF; @@ -24,10 +23,17 @@ import android.util.AttributeSet; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + /** * A view positioned under the notification shade. */ -public class LockIconView extends ImageView { +public class LockIconView extends ImageView implements Dumpable { @NonNull private final RectF mSensorRect; @NonNull private PointF mLockIconCenter = new PointF(0f, 0f); private int mRadius; @@ -37,7 +43,7 @@ public class LockIconView extends ImageView { mSensorRect = new RectF(); } - void setLocation(@NonNull PointF center, int radius) { + void setCenterLocation(@NonNull PointF center, int radius) { mLockIconCenter = center; mRadius = radius; @@ -63,4 +69,11 @@ public class LockIconView extends ImageView { float getLocationTop() { return mLockIconCenter.y - mRadius; } + + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("Center in px (x, y)= (" + mLockIconCenter.x + ", " + mLockIconCenter.y + ")"); + pw.println("Radius in pixels: " + mRadius); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index ccc487925fe4..62cb4b9a33f5 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -18,17 +18,21 @@ package com.android.keyguard; import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; -import static com.android.systemui.classifier.Classifier.DISABLED_UDFPS_AFFORDANCE; +import static com.android.systemui.classifier.Classifier.LOCK_ICON; import android.content.Context; +import android.content.res.Configuration; import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.hardware.biometrics.BiometricSourceType; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.util.Log; +import android.util.DisplayMetrics; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -73,15 +77,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull private final AccessibilityManager mAccessibilityManager; @NonNull private final ConfigurationController mConfigurationController; @NonNull private final DelayableExecutor mExecutor; - - private boolean mHasUdfpsOrFaceAuthFeatures; private boolean mUdfpsEnrolled; - private boolean mFaceAuthEnrolled; - @NonNull private final Drawable mButton; @NonNull private final Drawable mUnlockIcon; @NonNull private final Drawable mLockIcon; - @NonNull private final CharSequence mDisabledLabel; @NonNull private final CharSequence mUnlockedLabel; @NonNull private final CharSequence mLockedLabel; @@ -95,10 +94,18 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private boolean mUserUnlockedWithBiometric; private Runnable mCancelDelayedUpdateVisibilityRunnable; - private boolean mShowButton; + private boolean mHasUdfps; + private float mHeightPixels; + private float mWidthPixels; + private float mDensity; + private int mKgBottomAreaHeight; + private boolean mShowUnlockIcon; private boolean mShowLockIcon; + private boolean mDownDetected; + private final Rect mSensorTouchLocation = new Rect(); + @Inject public LockIconViewController( @Nullable LockIconView view, @@ -125,8 +132,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor = executor; final Context context = view.getContext(); - mButton = context.getResources().getDrawable( - com.android.systemui.R.drawable.circle_white, context.getTheme()); mUnlockIcon = new InsetDrawable(context.getResources().getDrawable( com.android.internal.R.drawable.ic_lock_open, context.getTheme()), context.getResources().getDimensionPixelSize( @@ -135,8 +140,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme com.android.internal.R.drawable.ic_lock, context.getTheme()), context.getResources().getDimensionPixelSize( com.android.systemui.R.dimen.udfps_unlock_icon_inset)); - mDisabledLabel = context.getResources().getString( - R.string.accessibility_udfps_disabled_button); mUnlockedLabel = context.getResources().getString(R.string.accessibility_unlock_button); mLockedLabel = context.getResources().getString(R.string.accessibility_lock_icon); dumpManager.registerDumpable("LockIconViewController", this); @@ -149,37 +152,11 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override protected void onViewAttached() { - // we check this here instead of onInit since the FingeprintManager + FaceManager may not + // we check this here instead of onInit since the FingerprintManager + FaceManager may not // have started up yet onInit - final boolean hasFaceAuth = mAuthController.getFaceAuthSensorLocation() != null; - final boolean hasUdfps = mAuthController.getUdfpsSensorLocation() != null; - mHasUdfpsOrFaceAuthFeatures = hasFaceAuth || hasUdfps; - if (!mHasUdfpsOrFaceAuthFeatures) { - // Posting since removing a view in the middle of onAttach can lead to a crash in the - // iteration loop when the view isn't last - mView.setVisibility(View.GONE); - mView.post(() -> { - mView.setVisibility(View.VISIBLE); - ((ViewGroup) mView.getParent()).removeView(mView); - }); - return; - } - - if (hasUdfps) { - FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0); - mView.setLocation(new PointF(props.sensorLocationX, props.sensorLocationY), - props.sensorRadius); - } else { - int[] props = mView.getContext().getResources().getIntArray( - com.android.systemui.R.array.config_lock_icon_props); - if (props == null || props.length < 3) { - Log.e("LockIconViewController", "lock icon position should be " - + "setup in config under config_lock_icon_props"); - props = new int[]{0, 0, 0}; - } - mView.setLocation(new PointF(props[0], props[1]), props[2]); - } + mHasUdfps = mAuthController.getUdfpsSensorLocation() != null; + updateConfiguration(); updateKeyguardShowing(); mUserUnlockedWithBiometric = false; mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); @@ -194,9 +171,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mStatusBarStateController.addCallback(mStatusBarStateListener); mKeyguardStateController.addCallback(mKeyguardStateCallback); - mAccessibilityManager.addTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - + mDownDetected = false; updateVisibility(); } @@ -206,8 +181,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); mStatusBarStateController.removeCallback(mStatusBarStateListener); mKeyguardStateController.removeCallback(mKeyguardStateCallback); - mAccessibilityManager.removeTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); if (mCancelDelayedUpdateVisibilityRunnable != null) { mCancelDelayedUpdateVisibilityRunnable.run(); @@ -219,18 +192,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme return mView.getLocationTop(); } - private boolean onAffordanceClick() { - if (mFalsingManager.isFalseTouch(DISABLED_UDFPS_AFFORDANCE)) { - return false; - } - - // pre-emptively set to true to hide view - mIsBouncerShowing = true; - updateVisibility(); - mKeyguardViewController.showBouncer(/* scrim */ true); - return true; - } - /** * Set whether qs is expanded. When QS is expanded, don't show a DisabledUdfps affordance. */ @@ -245,32 +206,24 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mCancelDelayedUpdateVisibilityRunnable = null; } - if (!mIsKeyguardShowing || (!mUdfpsEnrolled && !mFaceAuthEnrolled)) { + if (!mIsKeyguardShowing) { mView.setVisibility(View.INVISIBLE); return; } - // these three states are mutually exclusive: - mShowButton = mUdfpsEnrolled && !mCanDismissLockScreen && !mRunningFPS - && !mUserUnlockedWithBiometric && isLockScreen(); - mShowUnlockIcon = mFaceAuthEnrolled & mCanDismissLockScreen && isLockScreen(); - mShowLockIcon = !mUdfpsEnrolled && !mCanDismissLockScreen && isLockScreen() - && mFaceAuthEnrolled; + mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen() + && (!mUdfpsEnrolled || !mRunningFPS); + mShowUnlockIcon = mCanDismissLockScreen && isLockScreen(); - updateClickListener(); final CharSequence prevContentDescription = mView.getContentDescription(); - if (mShowButton) { - mView.setImageDrawable(mButton); + if (mShowLockIcon) { + mView.setImageDrawable(mLockIcon); mView.setVisibility(View.VISIBLE); - mView.setContentDescription(mDisabledLabel); + mView.setContentDescription(mLockedLabel); } else if (mShowUnlockIcon) { mView.setImageDrawable(mUnlockIcon); mView.setVisibility(View.VISIBLE); mView.setContentDescription(mUnlockedLabel); - } else if (mShowLockIcon) { - mView.setImageDrawable(mLockIcon); - mView.setVisibility(View.VISIBLE); - mView.setContentDescription(mLockedLabel); } else { mView.setVisibility(View.INVISIBLE); mView.setContentDescription(null); @@ -293,10 +246,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme getResources().getString(R.string.accessibility_enter_hint)); public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(v, info); - if (mShowButton || mShowLockIcon) { - info.addAction(mAccessibilityAuthenticateHint); - } else if (mShowUnlockIcon) { - info.addAction(mAccessibilityEnterHint); + if (isClickable()) { + if (mShowLockIcon) { + info.addAction(mAccessibilityAuthenticateHint); + } else if (mShowUnlockIcon) { + info.addAction(mAccessibilityEnterHint); + } } } }; @@ -308,18 +263,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme && mStatusBarState == StatusBarState.KEYGUARD; } - private void updateClickListener() { - if (mView != null) { - mView.setOnClickListener(v -> onAffordanceClick()); - if (mAccessibilityManager.isTouchExplorationEnabled()) { - mView.setOnLongClickListener(null); - mView.setLongClickable(false); - } else { - mView.setOnLongClickListener(v -> onAffordanceClick()); - } - } - } - private void updateKeyguardShowing() { mIsKeyguardShowing = mKeyguardStateController.isShowing() && !mKeyguardStateController.isKeyguardGoingAway(); @@ -332,13 +275,39 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mLockIcon.setTint(color); } + private void updateConfiguration() { + final DisplayMetrics metrics = mView.getContext().getResources().getDisplayMetrics(); + mWidthPixels = metrics.widthPixels; + mHeightPixels = metrics.heightPixels; + mDensity = metrics.density; + mKgBottomAreaHeight = mView.getContext().getResources().getDimensionPixelSize( + R.dimen.keyguard_indication_margin_bottom) + + mView.getContext().getResources().getDimensionPixelSize( + R.dimen.keyguard_indication_bottom_padding); + updateLockIconLocation(); + } + + private void updateLockIconLocation() { + if (mHasUdfps) { + FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0); + mView.setCenterLocation(new PointF(props.sensorLocationX, props.sensorLocationY), + props.sensorRadius); + } else { + final float distAboveKgBottomArea = 12 * mDensity; + final float radius = 36 * mDensity; + mView.setCenterLocation( + new PointF(mWidthPixels / 2, + mHeightPixels - mKgBottomAreaHeight - distAboveKgBottomArea + - radius / 2), (int) radius); + } + + mView.getHitRect(mSensorTouchLocation); + } + @Override public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { - pw.println("mHasUdfpsOrFaceAuthFeatures: " + mHasUdfpsOrFaceAuthFeatures); pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled); - pw.println("mFaceAuthEnrolled: " + mFaceAuthEnrolled); pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing); - pw.println(" mShowBouncerButton: " + mShowButton); pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mShowLockIcon: " + mShowLockIcon); pw.println(" mIsDozing: " + mIsDozing); @@ -348,6 +317,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); pw.println(" mStatusBarState: " + StatusBarState.toShortString(mStatusBarState)); pw.println(" mQsExpanded: " + mQsExpanded); + + if (mView != null) { + mView.dump(fd, pw, args); + } } private StatusBarStateController.StateListener mStatusBarStateListener = @@ -420,7 +393,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme public void onKeyguardShowingChanged() { updateKeyguardShowing(); mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled(); - mFaceAuthEnrolled = mKeyguardUpdateMonitor.isFaceEnrolled(); updateVisibility(); } @@ -447,10 +419,87 @@ public class LockIconViewController extends ViewController<LockIconView> impleme public void onOverlayChanged() { updateColors(); } + + @Override + public void onConfigChanged(Configuration newConfig) { + updateConfiguration(); + } }; - private final AccessibilityManager.TouchExplorationStateChangeListener - mTouchExplorationStateChangeListener = enabled -> updateClickListener(); + private final GestureDetector mGestureDetector = + new GestureDetector(new SimpleOnGestureListener() { + public boolean onDown(MotionEvent e) { + if (!isClickable()) { + mDownDetected = false; + return false; + } + + // intercept all following touches until we see MotionEvent.ACTION_CANCEL UP or + // MotionEvent.ACTION_UP (see #onTouchEvent) + mDownDetected = true; + return true; + } + + public void onLongPress(MotionEvent e) { + if (!wasClickableOnDownEvent()) { + return; + } + + onAffordanceClick(); + } + + public boolean onSingleTapUp(MotionEvent e) { + if (!wasClickableOnDownEvent()) { + return false; + } + + onAffordanceClick(); + return true; + } + + private boolean wasClickableOnDownEvent() { + return mDownDetected; + } + + private void onAffordanceClick() { + if (mFalsingManager.isFalseTouch(LOCK_ICON)) { + return; + } + + // pre-emptively set to true to hide view + mIsBouncerShowing = true; + updateVisibility(); + mKeyguardViewController.showBouncer(/* scrim */ true); + } + }); + + /** + * Send touch events to this view and handles it if the touch is within this view and we are + * in a 'clickable' state + * @return whether to intercept the touch event + */ + public boolean onTouchEvent(MotionEvent event) { + if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY()) + && mView.getVisibility() == View.VISIBLE) { + mGestureDetector.onTouchEvent(event); + } + + // we continue to intercept all following touches until we see MotionEvent.ACTION_CANCEL UP + // or MotionEvent.ACTION_UP. this is to avoid passing the touch to NPV + // after the lock icon disappears on device entry + if (mDownDetected) { + if (event.getAction() == MotionEvent.ACTION_CANCEL + || event.getAction() == MotionEvent.ACTION_UP) { + mDownDetected = false; + } + return true; + } + return false; + } + + private boolean isClickable() { + return mUdfpsEnrolled || mShowUnlockIcon; + } /** * Set the alpha of this view. diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 0d31906d2f80..099e6f4b5341 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -17,6 +17,7 @@ package com.android.keyguard; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.graphics.drawable.VectorDrawable; @@ -26,7 +27,6 @@ import android.view.MotionEvent; import androidx.annotation.Nullable; import com.android.settingslib.Utils; -import com.android.systemui.R; /** * Similar to the {@link NumPadKey}, but displays an image. @@ -35,6 +35,7 @@ public class NumPadButton extends AlphaOptimizedImageButton { @Nullable private NumPadAnimator mAnimator; + private int mOrientation; public NumPadButton(Context context, AttributeSet attrs) { super(context, attrs); @@ -49,13 +50,21 @@ public class NumPadButton extends AlphaOptimizedImageButton { } @Override + protected void onConfigurationChanged(Configuration newConfig) { + mOrientation = newConfig.orientation; + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Set width/height to the same value to ensure a smooth circle for the bg, but shrink // the height to match the old pin bouncer int width = getMeasuredWidth(); - int height = mAnimator == null ? (int) (width * .75f) : width; + + boolean shortenHeight = mAnimator == null + || mOrientation == Configuration.ORIENTATION_LANDSCAPE; + int height = shortenHeight ? (int) (width * .66f) : width; setMeasuredDimension(getMeasuredWidth(), height); } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java index cffa630c6ec8..232c6fc99068 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.keyguard; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; @@ -52,6 +52,7 @@ public class NumPadKey extends ViewGroup { @Nullable private NumPadAnimator mAnimator; + private int mOrientation; private View.OnClickListener mListener = new View.OnClickListener() { @Override @@ -139,6 +140,11 @@ public class NumPadKey extends ViewGroup { } } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + mOrientation = newConfig.orientation; + } + /** * Reload colors from resources. **/ @@ -171,7 +177,10 @@ public class NumPadKey extends ViewGroup { // Set width/height to the same value to ensure a smooth circle for the bg, but shrink // the height to match the old pin bouncer int width = getMeasuredWidth(); - int height = mAnimator == null ? (int) (width * .75f) : width; + + boolean shortenHeight = mAnimator == null + || mOrientation == Configuration.ORIENTATION_LANDSCAPE; + int height = shortenHeight ? (int) (width * .66f) : width; setMeasuredDimension(getMeasuredWidth(), height); } diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index b80f8bd64dcf..b1597144d237 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; @@ -87,7 +88,7 @@ public class PasswordTextView extends View { /** * The raw text size, will be multiplied by the scaled density when drawn */ - private final int mTextHeightRaw; + private int mTextHeightRaw; private final int mGravity; private ArrayList<CharState> mTextChars = new ArrayList<>(); private String mText = ""; @@ -147,6 +148,7 @@ public class PasswordTextView extends View { } finally { a.recycle(); } + mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); mDrawPaint.setTextAlign(Paint.Align.CENTER); mDrawPaint.setTypeface(Typeface.create( @@ -164,6 +166,12 @@ public class PasswordTextView extends View { } @Override + protected void onConfigurationChanged(Configuration newConfig) { + mTextHeightRaw = getContext().getResources().getInteger( + R.integer.scaled_password_text_size); + } + + @Override protected void onDraw(Canvas canvas) { float totalDrawingWidth = getDrawingWidth(); float currentDrawPosition; diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 67cf4812ba6b..104d711f46fb 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -73,6 +73,7 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.DevicePolicyManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; @@ -360,6 +361,7 @@ public class Dependency { @Inject Lazy<PrivacyDotViewController> mPrivacyDotViewControllerLazy; @Inject Lazy<EdgeBackGestureHandler> mEdgeBackGestureHandler; @Inject Lazy<UiEventLogger> mUiEventLogger; + @Inject Lazy<FeatureFlags> mFeatureFlagsLazy; @Inject public Dependency() { @@ -574,6 +576,7 @@ public class Dependency { mProviders.put(PrivacyDotViewController.class, mPrivacyDotViewControllerLazy::get); mProviders.put(EdgeBackGestureHandler.class, mEdgeBackGestureHandler::get); mProviders.put(UiEventLogger.class, mUiEventLogger::get); + mProviders.put(FeatureFlags.class, mFeatureFlagsLazy::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 7cd43eff8e2a..cff6cf1f53f1 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -19,6 +19,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; import android.content.Context; +import android.os.UserHandle; import android.text.TextUtils; import androidx.annotation.MainThread; @@ -40,11 +41,11 @@ public class AccessibilityFloatingMenuController implements AccessibilityButtonModeObserver.ModeChangedListener, AccessibilityButtonTargetsObserver.TargetsChangedListener { - private final Context mContext; private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver; private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private Context mContext; @VisibleForTesting IAccessibilityFloatingMenu mFloatingMenu; private int mBtnMode; @@ -79,6 +80,7 @@ public class AccessibilityFloatingMenuController implements @Override public void onUserSwitchComplete(int userId) { + mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0); mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); mBtnTargets = mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index 5bb55222e09b..17818cd9c7ee 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.util.MathUtils.constrain; import static android.util.MathUtils.sq; import static android.view.WindowInsets.Type.ime; @@ -38,7 +39,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.DisplayMetrics; @@ -49,8 +49,6 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Animation; import android.view.animation.OvershootInterpolator; import android.view.animation.TranslateAnimation; @@ -58,8 +56,10 @@ import android.widget.FrameLayout; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; @@ -200,6 +200,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout mListView = listView; mWindowManager = context.getSystemService(WindowManager.class); + mLastConfiguration = new Configuration(getResources().getConfiguration()); mAdapter = new AccessibilityTargetAdapter(mTargets); mUiHandler = createUiHandler(); mPosition = position; @@ -243,7 +244,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout } }); - mLastConfiguration = new Configuration(getResources().getConfiguration()); initListView(); updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment); @@ -331,54 +331,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout // Do Nothing } - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - setupAccessibilityActions(info); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - fadeIn(); - - final Rect bounds = getAvailableBounds(); - if (action == R.id.action_move_top_left) { - setShapeType(ShapeType.OVAL); - snapToLocation(bounds.left, bounds.top); - return true; - } - - if (action == R.id.action_move_top_right) { - setShapeType(ShapeType.OVAL); - snapToLocation(bounds.right, bounds.top); - return true; - } - - if (action == R.id.action_move_bottom_left) { - setShapeType(ShapeType.OVAL); - snapToLocation(bounds.left, bounds.bottom); - return true; - } - - if (action == R.id.action_move_bottom_right) { - setShapeType(ShapeType.OVAL); - snapToLocation(bounds.right, bounds.bottom); - return true; - } - - if (action == R.id.action_move_to_edge_and_hide) { - setShapeType(ShapeType.HALF_OVAL); - return true; - } - - if (action == R.id.action_move_out_edge_and_show) { - setShapeType(ShapeType.OVAL); - return true; - } - - return super.performAccessibilityAction(action, arguments); - } - void show() { if (isShowing()) { return; @@ -525,50 +477,15 @@ public class AccessibilityFloatingMenuView extends FrameLayout mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS); } - private void setupAccessibilityActions(AccessibilityNodeInfo info) { - final Resources res = mContext.getResources(); - final AccessibilityAction moveTopLeft = - new AccessibilityAction(R.id.action_move_top_left, - res.getString( - R.string.accessibility_floating_button_action_move_top_left)); - info.addAction(moveTopLeft); - - final AccessibilityAction moveTopRight = - new AccessibilityAction(R.id.action_move_top_right, - res.getString( - R.string.accessibility_floating_button_action_move_top_right)); - info.addAction(moveTopRight); - - final AccessibilityAction moveBottomLeft = - new AccessibilityAction(R.id.action_move_bottom_left, - res.getString( - R.string.accessibility_floating_button_action_move_bottom_left)); - info.addAction(moveBottomLeft); - - final AccessibilityAction moveBottomRight = - new AccessibilityAction(R.id.action_move_bottom_right, - res.getString( - R.string.accessibility_floating_button_action_move_bottom_right)); - info.addAction(moveBottomRight); - - final int moveEdgeId = mShapeType == ShapeType.OVAL - ? R.id.action_move_to_edge_and_hide - : R.id.action_move_out_edge_and_show; - final int moveEdgeTextResId = mShapeType == ShapeType.OVAL - ? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half - : R.string.accessibility_floating_button_action_move_out_edge_and_show; - final AccessibilityAction moveToOrOutEdge = - new AccessibilityAction(moveEdgeId, res.getString(moveEdgeTextResId)); - info.addAction(moveToOrOutEdge); - } - private boolean onTouched(MotionEvent event) { final int action = event.getAction(); final int currentX = (int) event.getX(); final int currentY = (int) event.getY(); + final int marginStartEnd = getMarginStartEndWith(mLastConfiguration); final Rect touchDelegateBounds = - new Rect(mMargin, mMargin, mMargin + getLayoutWidth(), mMargin + getLayoutHeight()); + new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(), + mMargin + getLayoutHeight()); if (action == MotionEvent.ACTION_DOWN && touchDelegateBounds.contains(currentX, currentY)) { mIsDownInEnlargedTouchArea = true; @@ -682,15 +599,22 @@ public class AccessibilityFloatingMenuView extends FrameLayout mListView.setLayoutManager(layoutManager); mListView.addOnItemTouchListener(this); mListView.animate().setInterpolator(new OvershootInterpolator()); - updateListView(); + mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) { + @NonNull + @Override + public AccessibilityDelegateCompat getItemDelegate() { + return new ItemDelegateCompat(this, + AccessibilityFloatingMenuView.this); + } + }); + + updateListViewWith(mLastConfiguration); addView(mListView); } - private void updateListView() { - final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams(); - layoutParams.setMargins(mMargin, mMargin, mMargin, mMargin); - mListView.setLayoutParams(layoutParams); + private void updateListViewWith(Configuration configuration) { + updateMarginWith(configuration); final int elevation = getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); @@ -719,13 +643,15 @@ public class AccessibilityFloatingMenuView extends FrameLayout @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + mLastConfiguration.setTo(newConfig); + final int diff = newConfig.diff(mLastConfiguration); if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) { updateAccessibilityTitle(mCurrentLayoutParams); } updateDimensions(); - updateListView(); + updateListViewWith(newConfig); updateItemViewWith(mSizeType); updateColor(); updateStrokeWith(newConfig.uiMode, mAlignment); @@ -733,8 +659,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); updateScrollModeWith(hasExceededMaxLayoutHeight()); setSystemGestureExclusion(); - - mLastConfiguration.setTo(newConfig); } @VisibleForTesting @@ -756,11 +680,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout } private int getMinWindowX() { - return -mMargin; + return -getMarginStartEndWith(mLastConfiguration); } private int getMaxWindowX() { - return mScreenWidth - mMargin - getLayoutWidth(); + return mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth(); } private int getMaxWindowY() { @@ -805,6 +729,15 @@ public class AccessibilityFloatingMenuView extends FrameLayout return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0; } + private void updateMarginWith(Configuration configuration) { + // Avoid overlapping with system bars under landscape mode, update the margins of the menu + // to align the edge of system bars. + final int marginStartEnd = getMarginStartEndWith(configuration); + final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams(); + layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin); + mListView.setLayoutParams(layoutParams); + } + private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) { final float halfWidth = getLayoutWidth() / 2.0f; final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth; @@ -896,6 +829,12 @@ public class AccessibilityFloatingMenuView extends FrameLayout return (mPadding + mIconHeight) * mTargets.size() + mPadding; } + private int getMarginStartEndWith(Configuration configuration) { + return configuration != null + && configuration.orientation == ORIENTATION_PORTRAIT + ? mMargin : 0; + } + private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) { return sizeType == SizeType.SMALL ? getSmallSizeResIdWith(itemCount) @@ -932,7 +871,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout } private int getWindowWidth() { - return mMargin * 2 + getLayoutWidth(); + return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth(); } private int getWindowHeight() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java new file mode 100644 index 000000000000..93b0676b4930 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2021 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.accessibility.floatingmenu; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; + +import com.android.systemui.R; +import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType; + +import java.lang.ref.WeakReference; + +/** + * An accessibility item delegate for the individual items of the list view + * {@link AccessibilityFloatingMenuView}. + */ +final class ItemDelegateCompat extends RecyclerViewAccessibilityDelegate.ItemDelegate { + private final WeakReference<AccessibilityFloatingMenuView> mMenuViewRef; + + ItemDelegateCompat(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate, + AccessibilityFloatingMenuView menuView) { + super(recyclerViewDelegate); + this.mMenuViewRef = new WeakReference<>(menuView); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + if (mMenuViewRef.get() == null) { + return; + } + final AccessibilityFloatingMenuView menuView = mMenuViewRef.get(); + + final Resources res = menuView.getResources(); + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left, + res.getString( + R.string.accessibility_floating_button_action_move_top_left)); + info.addAction(moveTopLeft); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_top_right, + res.getString( + R.string.accessibility_floating_button_action_move_top_right)); + info.addAction(moveTopRight); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_bottom_left, + res.getString( + R.string.accessibility_floating_button_action_move_bottom_left)); + info.addAction(moveBottomLeft); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_move_bottom_right, + res.getString( + R.string.accessibility_floating_button_action_move_bottom_right)); + info.addAction(moveBottomRight); + + final int moveEdgeId = menuView.isOvalShape() + ? R.id.action_move_to_edge_and_hide + : R.id.action_move_out_edge_and_show; + final int moveEdgeTextResId = menuView.isOvalShape() + ? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half + : R.string.accessibility_floating_button_action_move_out_edge_and_show; + final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId, + res.getString(moveEdgeTextResId)); + info.addAction(moveToOrOutEdge); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (mMenuViewRef.get() == null) { + return super.performAccessibilityAction(host, action, args); + } + final AccessibilityFloatingMenuView menuView = mMenuViewRef.get(); + + menuView.fadeIn(); + + final Rect bounds = menuView.getAvailableBounds(); + if (action == R.id.action_move_top_left) { + menuView.setShapeType(ShapeType.OVAL); + menuView.snapToLocation(bounds.left, bounds.top); + return true; + } + + if (action == R.id.action_move_top_right) { + menuView.setShapeType(ShapeType.OVAL); + menuView.snapToLocation(bounds.right, bounds.top); + return true; + } + + if (action == R.id.action_move_bottom_left) { + menuView.setShapeType(ShapeType.OVAL); + menuView.snapToLocation(bounds.left, bounds.bottom); + return true; + } + + if (action == R.id.action_move_bottom_right) { + menuView.setShapeType(ShapeType.OVAL); + menuView.snapToLocation(bounds.right, bounds.bottom); + return true; + } + + if (action == R.id.action_move_to_edge_and_hide) { + menuView.setShapeType(ShapeType.HALF_OVAL); + return true; + } + + if (action == R.id.action_move_out_edge_and_show) { + menuView.setShapeType(ShapeType.OVAL); + return true; + } + + return super.performAccessibilityAction(host, action, args); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index 534f93ec0e47..9676a57b2df9 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -64,7 +64,7 @@ import javax.inject.Inject; */ @SysUISingleton public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController, - AppOpsManager.OnOpActiveChangedInternalListener, + AppOpsManager.OnOpActiveChangedListener, AppOpsManager.OnOpNotedListener, IndividualSensorPrivacyController.Callback, Dumpable { @@ -359,11 +359,29 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active)); } + /** + * Required to override, delegate to other. Should not be called. + */ + public void onOpActiveChanged(String op, int uid, String packageName, boolean active) { + onOpActiveChanged(op, uid, packageName, null, active, + AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); + } + + // Get active app ops, and check if their attributions are trusted @Override - public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { + public void onOpActiveChanged(String op, int uid, String packageName, String attributionTag, + boolean active, int attributionFlags, int attributionChainId) { + int code = AppOpsManager.strOpToOp(op); if (DEBUG) { - Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s", code, uid, packageName, - Boolean.toString(active))); + Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName, + Boolean.toString(active), attributionChainId, attributionFlags)); + } + if (attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE + && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE + && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0 + && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) { + // if this attribution chain isn't trusted, and this isn't the accessor, do not show it. + return; } boolean activeChanged = updateActives(code, uid, packageName, active); if (!activeChanged) return; // early return diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index fe31a7b75c06..c9e67715decb 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -29,6 +29,7 @@ import android.os.UserHandle import android.util.Log import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper +import com.android.systemui.people.widget.PeopleBackupHelper /** * Helper for backing up elements in SystemUI @@ -45,18 +46,29 @@ class BackupHelper : BackupAgentHelper() { private const val TAG = "BackupHelper" internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite" + private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" private const val PERMISSION_SELF = "com.android.systemui.permission.SELF" } - override fun onCreate() { + override fun onCreate(userHandle: UserHandle, operationType: Int) { super.onCreate() // The map in mapOf is guaranteed to be order preserving val controlsMap = mapOf(CONTROLS to getPPControlsFile(this)) NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also { addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it) } + + // Conversations widgets backup only works for system user, because widgets' information is + // stored in system user's SharedPreferences files and we can't open those from other users. + if (!userHandle.isSystem) { + return + } + + val keys = PeopleBackupHelper.getFilesToBackup() + addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper( + this, userHandle, keys.toTypedArray())) } override fun onRestoreFinished() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java index 76fb49a730a3..205054d68280 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java @@ -19,17 +19,19 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; @@ -87,11 +89,10 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { } } - @Modality - private int mActiveSensorType = TYPE_FACE; - - @Nullable - private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter; + @Modality private int mActiveSensorType = TYPE_FACE; + @Nullable private ModalityListener mModalityListener; + @Nullable private FingerprintSensorPropertiesInternal mFingerprintSensorProps; + @Nullable private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter; public AuthBiometricFaceToFingerprintView(Context context) { super(context); @@ -106,14 +107,21 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { super(context, attrs, injector); } - void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { - if (!sensorProps.isAnyUdfpsType()) { - return; - } + @Modality + int getActiveSensorType() { + return mActiveSensorType; + } - if (mUdfpsMeasureAdapter == null || mUdfpsMeasureAdapter.getSensorProps() != sensorProps) { - mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); - } + boolean isFingerprintUdfps() { + return mFingerprintSensorProps.isAnyUdfpsType(); + } + + void setModalityListener(@NonNull ModalityListener listener) { + mModalityListener = listener; + } + + void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { + mFingerprintSensorProps = sensorProps; } @Override @@ -179,11 +187,16 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { @Override public void updateState(@BiometricState int newState) { if (mState == STATE_HELP || mState == STATE_ERROR) { + @Modality final int currentType = mActiveSensorType; mActiveSensorType = TYPE_FINGERPRINT; setRequireConfirmation(false); mConfirmButton.setEnabled(false); mConfirmButton.setVisibility(View.GONE); + + if (mModalityListener != null && currentType != mActiveSensorType) { + mModalityListener.onModalitySwitched(currentType, mActiveSensorType); + } } super.updateState(newState); @@ -193,8 +206,34 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { @NonNull AuthDialog.LayoutParams onMeasureInternal(int width, int height) { final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); - return mUdfpsMeasureAdapter != null - ? mUdfpsMeasureAdapter.onMeasureInternal(width, height, layoutParams) + return isFingerprintUdfps() + ? getUdfpsMeasureAdapter().onMeasureInternal(width, height, layoutParams) : layoutParams; } + + @NonNull + private UdfpsDialogMeasureAdapter getUdfpsMeasureAdapter() { + if (mUdfpsMeasureAdapter == null + || mUdfpsMeasureAdapter.getSensorProps() != mFingerprintSensorProps) { + mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, mFingerprintSensorProps); + } + return mUdfpsMeasureAdapter; + } + + @Override + public void onSaveState(@NonNull Bundle outState) { + super.onSaveState(outState); + outState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, mActiveSensorType); + outState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS, mFingerprintSensorProps); + } + + @Override + public void restoreState(@Nullable Bundle savedState) { + super.restoreState(savedState); + if (savedState != null) { + mActiveSensorType = savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FACE); + mFingerprintSensorProps = + savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index f37495ef5f48..bebf813e1833 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -593,11 +593,13 @@ public abstract class AuthBiometricView extends LinearLayout { } public void onSaveState(@NonNull Bundle outState) { + outState.putInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY, + mConfirmButton.getVisibility()); outState.putInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); outState.putInt(AuthDialog.KEY_BIOMETRIC_STATE, mState); outState.putString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING, - mIndicatorView.getText().toString()); + mIndicatorView.getText() != null ? mIndicatorView.getText().toString() : ""); outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING, mHandler.hasCallbacks(mResetErrorRunnable)); outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING, @@ -754,9 +756,15 @@ public abstract class AuthBiometricView extends LinearLayout { // Restore as much state as possible first updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); - // Restore positive button state + // Restore positive button(s) state + mConfirmButton.setVisibility( + mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY)); + if (mConfirmButton.getVisibility() == View.GONE) { + setRequireConfirmation(false); + } mTryAgainButton.setVisibility( mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index c4f78e7782a2..3f61d3c6af9a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import android.annotation.IntDef; @@ -35,6 +36,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserManager; import android.util.Log; +import android.view.Display; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -354,6 +356,12 @@ public class AuthContainerView extends LinearLayout (AuthBiometricFaceToFingerprintView) factory.inflate( R.layout.auth_biometric_face_to_fingerprint_view, null, false); faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps); + faceToFingerprintView.setModalityListener(new ModalityListener() { + @Override + public void onModalitySwitched(int oldModality, int newModality) { + maybeUpdatePositionForUdfps(true /* invalidate */); + } + }); mBiometricView = faceToFingerprintView; } else { Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId); @@ -469,6 +477,11 @@ public class AuthContainerView extends LinearLayout } @Override + public void onOrientationChanged() { + maybeUpdatePositionForUdfps(true /* invalidate */); + } + + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); onAttachedToWindowInternal(); @@ -487,32 +500,7 @@ public class AuthContainerView extends LinearLayout + mConfig.mPromptInfo.getAuthenticators()); } - if (mBiometricView instanceof AuthBiometricUdfpsView) { - final int displayRotation = getDisplay().getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - - case Surface.ROTATION_90: - mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); - break; - - case Surface.ROTATION_270: - mPanelController.setPosition(AuthPanelController.POSITION_LEFT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); - break; - - case Surface.ROTATION_180: - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - } - } + maybeUpdatePositionForUdfps(false /* invalidate */); if (mConfig.mSkipIntro) { mContainerState = STATE_SHOWING; @@ -557,6 +545,63 @@ public class AuthContainerView extends LinearLayout } } + private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { + if (view instanceof AuthBiometricUdfpsView) { + return true; + } + + if (view instanceof AuthBiometricFaceToFingerprintView) { + AuthBiometricFaceToFingerprintView faceToFingerprintView = + (AuthBiometricFaceToFingerprintView) view; + return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT + && faceToFingerprintView.isFingerprintUdfps(); + } + + return false; + } + + private boolean maybeUpdatePositionForUdfps(boolean invalidate) { + final Display display = getDisplay(); + if (display == null) { + return false; + } + if (!shouldUpdatePositionForUdfps(mBiometricView)) { + return false; + } + + final int displayRotation = display.getRotation(); + switch (displayRotation) { + case Surface.ROTATION_0: + mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); + setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + break; + + case Surface.ROTATION_90: + mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); + setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); + break; + + case Surface.ROTATION_270: + mPanelController.setPosition(AuthPanelController.POSITION_LEFT); + setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); + break; + + case Surface.ROTATION_180: + default: + Log.e(TAG, "Unsupported display rotation: " + displayRotation); + mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); + setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + break; + } + + if (invalidate) { + mPanelView.invalidateOutline(); + mBiometricView.requestLayout(); + } + + return true; + } + private void setScrollViewGravity(int gravity) { final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 7947241ff794..2aa89e86d5c8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -30,7 +30,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.PointF; -import android.graphics.RectF; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager.Authenticators; @@ -49,7 +48,10 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.util.Log; +import android.view.Display; import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.Surface; import android.view.WindowManager; import com.android.internal.R; @@ -97,17 +99,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @VisibleForTesting AuthDialog mCurrentDialog; - private WindowManager mWindowManager; - @Nullable - private UdfpsController mUdfpsController; - @Nullable - private IUdfpsHbmListener mUdfpsHbmListener; - @Nullable - private SidefpsController mSidefpsController; + @NonNull private final WindowManager mWindowManager; + @Nullable private UdfpsController mUdfpsController; + @Nullable private IUdfpsHbmListener mUdfpsHbmListener; + @Nullable private SidefpsController mSidefpsController; @VisibleForTesting TaskStackListener mTaskStackListener; @VisibleForTesting IBiometricSysuiReceiver mReceiver; + @NonNull private final BiometricOrientationEventListener mOrientationListener; @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps; @Nullable private List<FingerprintSensorPropertiesInternal> mFpProps; @Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps; @@ -120,6 +120,46 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } } + private class BiometricOrientationEventListener extends OrientationEventListener { + @Surface.Rotation private int mLastRotation = ORIENTATION_UNKNOWN; + + BiometricOrientationEventListener(Context context) { + super(context); + + final Display display = context.getDisplay(); + if (display != null) { + mLastRotation = display.getRotation(); + } + } + + @Override + public void onOrientationChanged(int orientation) { + if (orientation == ORIENTATION_UNKNOWN) { + return; + } + + final Display display = mContext.getDisplay(); + if (display == null) { + return; + } + + final int rotation = display.getRotation(); + if (mLastRotation != rotation) { + mLastRotation = rotation; + + if (mCurrentDialog != null) { + mCurrentDialog.onOrientationChanged(); + } + if (mUdfpsController != null) { + mUdfpsController.onOrientationChanged(); + } + if (mSidefpsController != null) { + mSidefpsController.onOrientationChanged(); + } + } + } + } + @NonNull private final IFingerprintAuthenticatorsRegisteredCallback mFingerprintAuthenticatorsRegisteredCallback = @@ -192,6 +232,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, Log.w(TAG, "Evicting client due to: " + topPackage); mCurrentDialog.dismissWithoutCallback(true /* animate */); mCurrentDialog = null; + if (mReceiver != null) { mReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_USER_CANCEL, @@ -342,15 +383,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, /** * @return where the UDFPS exists on the screen in pixels in portrait mode. */ - @Nullable public RectF getUdfpsRegion() { - return mUdfpsController == null - ? null - : mUdfpsController.getSensorLocation(); - } - - /** - * @return where the UDFPS exists on the screen in pixels in portrait mode. - */ @Nullable public PointF getUdfpsSensorLocation() { if (mUdfpsController == null) { return null; @@ -422,8 +454,10 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } @Inject - public AuthController(Context context, CommandQueue commandQueue, + public AuthController(Context context, + CommandQueue commandQueue, ActivityTaskManager activityTaskManager, + @NonNull WindowManager windowManager, @Nullable FingerprintManager fingerprintManager, @Nullable FaceManager faceManager, Provider<UdfpsController> udfpsControllerFactory, @@ -435,6 +469,9 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, mFaceManager = faceManager; mUdfpsControllerFactory = udfpsControllerFactory; mSidefpsControllerFactory = sidefpsControllerFactory; + mWindowManager = windowManager; + mOrientationListener = new BiometricOrientationEventListener(context); + mOrientationListener.enable(); mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null; @@ -462,7 +499,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override public void start() { mCommandQueue.addCallback(this); - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); if (mFingerprintManager != null) { mFingerprintManager.addAuthenticatorsRegisteredCallback( @@ -715,15 +751,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - // UdfpsController is not BiometricPrompt-specific. It can be active for keyguard or - // enrollment. - if (mUdfpsController != null) { - mUdfpsController.onConfigurationChanged(); - } - - if (mSidefpsController != null) { - mSidefpsController.onConfigurationChanged(); - } // Save the state of the current dialog (buttons showing, etc) if (mCurrentDialog != null) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index cbd166e6e0b1..fa5213e94081 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -35,6 +35,7 @@ public interface AuthDialog { String KEY_BIOMETRIC_SHOWING = "biometric_showing"; String KEY_CREDENTIAL_SHOWING = "credential_showing"; + String KEY_BIOMETRIC_CONFIRM_VISIBILITY = "confirm_visibility"; String KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY = "try_agian_visibility"; String KEY_BIOMETRIC_STATE = "state"; String KEY_BIOMETRIC_INDICATOR_STRING = "indicator_string"; // error / help / hint @@ -42,6 +43,9 @@ public interface AuthDialog { String KEY_BIOMETRIC_INDICATOR_HELP_SHOWING = "hint_is_temporary"; String KEY_BIOMETRIC_DIALOG_SIZE = "size"; + String KEY_BIOMETRIC_SENSOR_TYPE = "sensor_type"; + String KEY_BIOMETRIC_SENSOR_PROPS = "sensor_props"; + int SIZE_UNKNOWN = 0; /** * Minimal UI, showing only biometric icon. @@ -152,4 +156,12 @@ public interface AuthDialog { * @return true if device credential is allowed. */ boolean isAllowDeviceCredentials(); + + /** + * Called when the device's orientation changed and the dialog may need to do another + * layout. This is most relevant to UDFPS since configuration changes are not sent by + * the framework in equivalent cases (landscape to reverse landscape) but the dialog + * must remain fixed on the physical sensor location. + */ + void onOrientationChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 77cca2e3089c..1df8ad5e51fb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -25,7 +25,6 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.statusbar.CircleReveal -import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.commandline.Command @@ -116,9 +115,6 @@ class AuthRippleController @Inject constructor( /* end runnable */ Runnable { notificationShadeWindowController.setForcePluginOpen(false, this) - if (useCircleReveal) { - lightRevealScrim?.revealEffect = LiftReveal - } }, /* circleReveal */ if (useCircleReveal) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index dd73c4f8d071..95ea81003ecb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -23,11 +23,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.PointF -import android.media.AudioAttributes -import android.os.VibrationEffect -import android.os.Vibrator import android.util.AttributeSet -import android.util.MathUtils import android.view.View import android.view.animation.PathInterpolator import com.android.internal.graphics.ColorUtils @@ -36,26 +32,25 @@ import com.android.systemui.statusbar.charging.RippleShader private const val RIPPLE_ANIMATION_DURATION: Long = 1533 private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f -private const val RIPPLE_VIBRATION_PRIMITIVE: Int = VibrationEffect.Composition.PRIMITIVE_LOW_TICK -private const val RIPPLE_VIBRATION_SIZE: Int = 60 -private const val RIPPLE_VIBRATION_SCALE_START: Float = 0.6f -private const val RIPPLE_VIBRATION_SCALE_DECAY: Float = -0.1f /** * Expanding ripple effect on the transition from biometric authentication success to showing * launcher. */ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { - private val vibrator: Vibrator? = context?.getSystemService(Vibrator::class.java) private var rippleInProgress: Boolean = false private val rippleShader = RippleShader() private val ripplePaint = Paint() - private val rippleVibrationEffect = createVibrationEffect(vibrator) - private val rippleVibrationAttrs = - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .build() + private var radius: Float = 0.0f + set(value) { + rippleShader.radius = value + field = value + } + private var origin: PointF = PointF() + set(value) { + rippleShader.origin = value + field = value + } init { rippleShader.color = 0xffffffff.toInt() // default color @@ -66,8 +61,8 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } fun setSensorLocation(location: PointF) { - rippleShader.origin = location - rippleShader.radius = maxOf(location.x, location.y, width - location.x, height - location.y) + origin = location + radius = maxOf(location.x, location.y, width - location.x, height - location.y) .toFloat() } @@ -141,8 +136,6 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } }) } - // TODO (b/185124905): custom haptic TBD - // vibrate() animatorSet.start() } @@ -151,26 +144,11 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } override fun onDraw(canvas: Canvas?) { - // draw over the entire screen - canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), ripplePaint) - } - - private fun vibrate() { - if (rippleVibrationEffect != null) { - vibrator?.vibrate(rippleVibrationEffect, rippleVibrationAttrs) - } - } - - private fun createVibrationEffect(vibrator: Vibrator?): VibrationEffect? { - if (vibrator?.areAllPrimitivesSupported(RIPPLE_VIBRATION_PRIMITIVE) == false) { - return null - } - val composition = VibrationEffect.startComposition() - for (i in 0 until RIPPLE_VIBRATION_SIZE) { - val scale = - RIPPLE_VIBRATION_SCALE_START * MathUtils.exp(RIPPLE_VIBRATION_SCALE_DECAY * i) - composition.addPrimitive(RIPPLE_VIBRATION_PRIMITIVE, scale, 0 /* delay */) - } - return composition.compose() + // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover + // the active effect area. Values here should be kept in sync with the + // animation implementation in the ripple shader. + val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * radius * 1.5f + canvas?.drawCircle(origin.x, origin.y, maskRadius, ripplePaint) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ModalityListener.java b/packages/SystemUI/src/com/android/systemui/biometrics/ModalityListener.java new file mode 100644 index 000000000000..c162f7dec452 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ModalityListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import android.hardware.biometrics.BiometricAuthenticator.Modality; + +/** + * Listener for events related to modality changes during operations. + * + * Used by views such as {@link AuthBiometricFaceToFingerprintView} that support fallback style + * authentication. + */ +public interface ModalityListener { + + /** + * The modality has changed. Called after the transition has been fully completed. + * + * @param oldModality original modality + * @param newModality current modality + */ + default void onModalitySwitched(@Modality int oldModality, @Modality int newModality) {} +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java index a52296a71960..436e1e4e3116 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java @@ -137,8 +137,7 @@ public class SidefpsController { } } - - void onConfigurationChanged() { + void onOrientationChanged() { // If mView is null or if view is hidden, then return. if (mView == null || !mIsVisible) { return; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java index 2d403f63c2cd..1f11894de55e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java @@ -74,7 +74,10 @@ abstract class UdfpsAnimationView extends FrameLayout { return false; } - protected void updateAlpha() { + /** + * @return current alpha + */ + protected int updateAlpha() { int alpha = calculateAlpha(); getDrawable().setAlpha(alpha); @@ -84,6 +87,8 @@ abstract class UdfpsAnimationView extends FrameLayout { } else { ((ViewGroup) getParent()).setVisibility(View.VISIBLE); } + + return alpha; } int calculateAlpha() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 11412f41f578..81e60f316bbd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -16,6 +16,8 @@ package com.android.systemui.biometrics; +import static android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK; + import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION; @@ -71,6 +73,7 @@ import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.concurrency.Execution; import java.util.Optional; @@ -97,6 +100,7 @@ public class UdfpsController implements DozeReceiver { private static final long MIN_TOUCH_LOG_INTERVAL = 50; private final Context mContext; + private final Execution mExecution; private final FingerprintManager mFingerprintManager; @NonNull private final LayoutInflater mInflater; private final WindowManager mWindowManager; @@ -143,34 +147,25 @@ public class UdfpsController implements DozeReceiver { @Nullable private Runnable mCancelAodTimeoutAction; private boolean mScreenOn; private Runnable mAodInterruptRunnable; + private boolean mOnFingerDown; @VisibleForTesting - static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES = + public static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); - private final VibrationEffect mEffectTick = VibrationEffect.get(VibrationEffect.EFFECT_TICK); - private final VibrationEffect mEffectTextureTick = + public static final VibrationEffect EFFECT_TICK = + VibrationEffect.get(VibrationEffect.EFFECT_TICK); + private static final VibrationEffect EFFECT_TEXTURE_TICK = VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK); @VisibleForTesting - final VibrationEffect mEffectClick = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - private final VibrationEffect mEffectHeavy = + static final VibrationEffect EFFECT_CLICK = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + private static final VibrationEffect EFFECT_HEAVY = VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); - private final VibrationEffect mDoubleClick = + private static final VibrationEffect EFFECT_DOUBLE_CLICK = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); - private final Runnable mAcquiredVibration = new Runnable() { - @Override - public void run() { - if (mVibrator == null) { - return; - } - String effect = Settings.Global.getString(mContext.getContentResolver(), - "udfps_acquired_type"); - mVibrator.vibrate(getVibration(effect, mEffectTick), VIBRATION_SONIFICATION_ATTRIBUTES); - } - }; private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override @@ -436,30 +431,7 @@ public class UdfpsController implements DozeReceiver { mTouchLogTime = SystemClock.elapsedRealtime(); mPowerManager.userActivity(SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0); - - // TODO: this should eventually be removed after ux testing - if (mVibrator != null) { - final ContentResolver contentResolver = - mContext.getContentResolver(); - int startEnabled = Settings.Global.getInt(contentResolver, - "udfps_start", 1); - if (startEnabled > 0) { - String startEffectSetting = Settings.Global.getString( - contentResolver, "udfps_start_type"); - mVibrator.vibrate(getVibration(startEffectSetting, - mEffectClick), VIBRATION_SONIFICATION_ATTRIBUTES); - } - - int acquiredEnabled = Settings.Global.getInt(contentResolver, - "udfps_acquired", 0); - if (acquiredEnabled > 0) { - int delay = Settings.Global.getInt(contentResolver, - "udfps_acquired_delay", 500); - mMainHandler.removeCallbacks(mAcquiredVibration); - mMainHandler.postDelayed(mAcquiredVibration, delay); - } - } - + playStartHaptic(); handled = true; } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) { Log.v(TAG, "onTouch | finger move: " + touchInfo); @@ -496,6 +468,7 @@ public class UdfpsController implements DozeReceiver { @Inject public UdfpsController(@NonNull Context context, + @NonNull Execution execution, @NonNull LayoutInflater inflater, @Nullable FingerprintManager fingerprintManager, @NonNull WindowManager windowManager, @@ -512,8 +485,10 @@ public class UdfpsController implements DozeReceiver { @NonNull LockscreenShadeTransitionController lockscreenShadeTransitionController, @NonNull ScreenLifecycle screenLifecycle, @Nullable Vibrator vibrator, + @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, @NonNull Optional<UdfpsHbmProvider> hbmProvider) { mContext = context; + mExecution = execution; // TODO (b/185124905): inject main handler and vibrator once done prototyping mMainHandler = new Handler(Looper.getMainLooper()); mVibrator = vibrator; @@ -557,6 +532,29 @@ public class UdfpsController implements DozeReceiver { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(mBroadcastReceiver, filter); + + udfpsHapticsSimulator.setUdfpsController(this); + } + + /** + * Play haptic to signal udfps scanning started. + */ + @VisibleForTesting + public void playStartHaptic() { + if (mVibrator != null) { + final ContentResolver contentResolver = + mContext.getContentResolver(); + // TODO: these settings checks should eventually be removed after ux testing + // (b/185124905) + int startEnabled = Settings.Global.getInt(contentResolver, + "udfps_start", 1); + if (startEnabled > 0) { + String startEffectSetting = Settings.Global.getString( + contentResolver, "udfps_start_type"); + mVibrator.vibrate(getVibration(startEffectSetting, + EFFECT_CLICK), VIBRATION_SONIFICATION_ATTRIBUTES); + } + } } private int getCoreLayoutParamFlags() { @@ -599,8 +597,10 @@ public class UdfpsController implements DozeReceiver { } private void updateOverlay() { + mExecution.assertIsMainThread(); + if (mServerRequest != null) { - showUdfpsOverlay(mServerRequest.mRequestReason); + showUdfpsOverlay(mServerRequest); } else { hideUdfpsOverlay(); } @@ -650,7 +650,7 @@ public class UdfpsController implements DozeReceiver { return mCoreLayoutParams; } - void onConfigurationChanged() { + void onOrientationChanged() { // When the configuration changes it's almost always necessary to destroy and re-create // the overlay's window to pass it the new LayoutParams. // Hiding the overlay will destroy its window. It's safe to hide the overlay regardless @@ -661,36 +661,38 @@ public class UdfpsController implements DozeReceiver { updateOverlay(); } - private void showUdfpsOverlay(int reason) { - mFgExecutor.execute(() -> { - if (mView == null) { - try { - Log.v(TAG, "showUdfpsOverlay | adding window reason=" + reason); - mView = (UdfpsView) mInflater.inflate(R.layout.udfps_view, null, false); - mView.setSensorProperties(mSensorProps); - mView.setHbmProvider(mHbmProvider); - UdfpsAnimationViewController animation = inflateUdfpsAnimation(reason); - animation.init(); - mView.setAnimationViewController(animation); - - // This view overlaps the sensor area, so prevent it from being selectable - // during a11y. - if (reason == IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR - || reason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING) { - mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } + private void showUdfpsOverlay(@NonNull ServerRequest request) { + mExecution.assertIsMainThread(); - mWindowManager.addView(mView, computeLayoutParams(animation)); - mAccessibilityManager.addTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - updateTouchListener(); - } catch (RuntimeException e) { - Log.e(TAG, "showUdfpsOverlay | failed to add window", e); + final int reason = request.mRequestReason; + if (mView == null) { + try { + Log.v(TAG, "showUdfpsOverlay | adding window reason=" + reason); + mView = (UdfpsView) mInflater.inflate(R.layout.udfps_view, null, false); + mOnFingerDown = false; + mView.setSensorProperties(mSensorProps); + mView.setHbmProvider(mHbmProvider); + UdfpsAnimationViewController animation = inflateUdfpsAnimation(reason); + animation.init(); + mView.setAnimationViewController(animation); + + // This view overlaps the sensor area, so prevent it from being selectable + // during a11y. + if (reason == IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR + || reason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING) { + mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); } - } else { - Log.v(TAG, "showUdfpsOverlay | the overlay is already showing"); + + mWindowManager.addView(mView, computeLayoutParams(animation)); + mAccessibilityManager.addTouchExplorationStateChangeListener( + mTouchExplorationStateChangeListener); + updateTouchListener(); + } catch (RuntimeException e) { + Log.e(TAG, "showUdfpsOverlay | failed to add window", e); } - }); + } else { + Log.v(TAG, "showUdfpsOverlay | the overlay is already showing"); + } } private UdfpsAnimationViewController inflateUdfpsAnimation(int reason) { @@ -750,22 +752,22 @@ public class UdfpsController implements DozeReceiver { } private void hideUdfpsOverlay() { - mFgExecutor.execute(() -> { - if (mView != null) { - Log.v(TAG, "hideUdfpsOverlay | removing window"); - // Reset the controller back to its starting state. - onFingerUp(); - mWindowManager.removeView(mView); - mView.setOnTouchListener(null); - mView.setOnHoverListener(null); - mView.setAnimationViewController(null); - mAccessibilityManager.removeTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - mView = null; - } else { - Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); - } - }); + mExecution.assertIsMainThread(); + + if (mView != null) { + Log.v(TAG, "hideUdfpsOverlay | removing window"); + // Reset the controller back to its starting state. + onFingerUp(); + mWindowManager.removeView(mView); + mView.setOnTouchListener(null); + mView.setOnHoverListener(null); + mView.setAnimationViewController(null); + mAccessibilityManager.removeTouchExplorationStateChangeListener( + mTouchExplorationStateChangeListener); + mView = null; + } else { + Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); + } } /** @@ -820,12 +822,13 @@ public class UdfpsController implements DozeReceiver { mIsAodInterruptActive = false; } - // This method can be called from the UI thread. private void onFingerDown(int x, int y, float minor, float major) { + mExecution.assertIsMainThread(); if (mView == null) { Log.w(TAG, "Null view in onFingerDown"); return; } + mOnFingerDown = true; mFingerprintManager.onPointerDown(mSensorProps.sensorId, x, y, minor, major); Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); Trace.beginAsyncSection("UdfpsController.e2e.startIllumination", 0); @@ -835,39 +838,51 @@ public class UdfpsController implements DozeReceiver { }); } - // This method can be called from the UI thread. private void onFingerUp() { + mExecution.assertIsMainThread(); mActivePointerId = -1; mGoodCaptureReceived = false; - mMainHandler.removeCallbacks(mAcquiredVibration); if (mView == null) { Log.w(TAG, "Null view in onFingerUp"); return; } - mFingerprintManager.onPointerUp(mSensorProps.sensorId); + if (mOnFingerDown) { + mFingerprintManager.onPointerUp(mSensorProps.sensorId); + } + mOnFingerDown = false; if (mView.isIlluminationRequested()) { mView.stopIllumination(); } } - - private VibrationEffect getVibration(String effect, VibrationEffect defaultEffect) { + /** + * get vibration to play given string + * used for testing purposes (b/185124905) + */ + public static VibrationEffect getVibration(String effect, VibrationEffect defaultEffect) { if (TextUtils.isEmpty(effect)) { return defaultEffect; } switch (effect.toLowerCase()) { case "click": - return mEffectClick; + return EFFECT_CLICK; case "heavy": - return mEffectHeavy; + return EFFECT_HEAVY; case "texture_tick": - return mEffectTextureTick; + return EFFECT_TEXTURE_TICK; case "tick": - return mEffectTick; + return EFFECT_TICK; case "double_tap": - return mDoubleClick; + return EFFECT_DOUBLE_CLICK; default: + try { + int primitive = Integer.parseInt(effect); + if (primitive <= PRIMITIVE_LOW_TICK && primitive > -1) { + return VibrationEffect.startComposition().addPrimitive(primitive).compose(); + } + } catch (NumberFormatException e) { + } return defaultEffect; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java index 1ad2b9ca856c..7ccfb865cd5a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -180,17 +180,25 @@ public class UdfpsDialogMeasureAdapter { iconFrame.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_above_icon || child.getId() == R.id.button_bar) { - // Adjust the width of the top spacer and button bar while preserving their heights. + } else if (child.getId() == R.id.space_above_icon) { + // Adjust the width and height of the top spacer if necessary. + final int newTopSpacerHeight = child.getLayoutParams().height + - Math.min(bottomSpacerHeight, 0); + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.button_bar) { + // Adjust the width of the button bar while preserving its height. child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( child.getLayoutParams().height, MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.space_below_icon) { // Adjust the bottom spacer height to align the fingerprint icon with the sensor. + final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0); child.measure( MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); + MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY)); } else { // Use the remeasured width for all other child views. child.measure( @@ -208,7 +216,7 @@ public class UdfpsDialogMeasureAdapter { private int getViewHeightPx(@IdRes int viewId) { final View view = mView.findViewById(viewId); - return view != null ? view.getMeasuredHeight() : 0; + return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0; } private int getDialogMarginPx() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java index 7f4d7fe01e90..2cdf49d6fc3c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java @@ -43,11 +43,6 @@ public class UdfpsEnrollView extends UdfpsAnimationView { } @Override - protected void updateAlpha() { - super.updateAlpha(); - } - - @Override protected void onFinishInflate() { mFingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view); mFingerprintView.setImageDrawable(mFingerprintDrawable); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java index 91cc149be800..3dab010d917c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java @@ -71,12 +71,6 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp } } - @Override - protected void onViewDetached() { - super.onViewDetached(); - mEnrollHelper.setListener(null); - } - @NonNull @Override public PointF getTouchTranslation() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt new file mode 100644 index 000000000000..ea2bbfad1b74 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.media.AudioAttributes +import android.os.VibrationEffect +import android.os.Vibrator + +import com.android.keyguard.KeyguardUpdateMonitor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry + +import java.io.PrintWriter + +import javax.inject.Inject + +/** + * Used to simulate haptics that may be used for udfps authentication. + */ +@SysUISingleton +class UdfpsHapticsSimulator @Inject constructor( + commandRegistry: CommandRegistry, + val vibrator: Vibrator?, + val keyguardUpdateMonitor: KeyguardUpdateMonitor +) : Command { + val sonificationEffects = + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .build() + var udfpsController: UdfpsController? = null + + init { + commandRegistry.registerCommand("udfps-haptic") { this } + } + + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + invalidCommand(pw) + } else { + when (args[0]) { + "start" -> { + udfpsController?.playStartHaptic() + } + "acquired" -> { + keyguardUpdateMonitor.playAcquiredHaptic() + } + "success" -> { + // needs to be kept up to date with AcquisitionClient#SUCCESS_VIBRATION_EFFECT + vibrator?.vibrate( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + sonificationEffects) + } + "error" -> { + // needs to be kept up to date with AcquisitionClient#ERROR_VIBRATION_EFFECT + vibrator?.vibrate( + VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), + sonificationEffects) + } + else -> invalidCommand(pw) + } + } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar udfps-haptic <haptic>") + pw.println("Available commands:") + pw.println(" start") + pw.println(" acquired") + pw.println(" success, always plays CLICK haptic") + pw.println(" error, always plays DOUBLE_CLICK haptic") + } + + fun invalidCommand(pw: PrintWriter) { + pw.println("invalid command") + help(pw) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java deleted file mode 100644 index 889409351a8c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.util.MathUtils; - -import androidx.annotation.NonNull; - -import com.android.internal.graphics.ColorUtils; -import com.android.settingslib.Utils; -import com.android.systemui.R; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.doze.DozeReceiver; - -/** - * UDFPS animations that should be shown when authenticating on keyguard. - */ -public class UdfpsKeyguardDrawable extends UdfpsDrawable implements DozeReceiver { - - private static final String TAG = "UdfpsAnimationKeyguard"; - private final int mAmbientDisplayColor; - static final float DEFAULT_AOD_STROKE_WIDTH = 1f; - - @NonNull private final Context mContext; - private int mLockScreenColor; - - // AOD anti-burn-in offsets - private final int mMaxBurnInOffsetX; - private final int mMaxBurnInOffsetY; - private float mInterpolatedDarkAmount; - private float mBurnInOffsetX; - private float mBurnInOffsetY; - - private final ValueAnimator mHintAnimator = ValueAnimator.ofFloat( - UdfpsKeyguardDrawable.DEFAULT_STROKE_WIDTH, - .5f, - UdfpsKeyguardDrawable.DEFAULT_STROKE_WIDTH); - - UdfpsKeyguardDrawable(@NonNull Context context) { - super(context); - mContext = context; - - mMaxBurnInOffsetX = context.getResources() - .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); - mMaxBurnInOffsetY = context.getResources() - .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); - - mHintAnimator.setDuration(2000); - mHintAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - mHintAnimator.addUpdateListener(anim -> setStrokeWidth((float) anim.getAnimatedValue())); - - mLockScreenColor = Utils.getColorAttrDefaultColor(mContext, - R.attr.wallpaperTextColorAccent); - mAmbientDisplayColor = Color.WHITE; - - updateIcon(); - } - - private void updateIcon() { - mBurnInOffsetX = MathUtils.lerp(0f, - getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */) - - mMaxBurnInOffsetX, - mInterpolatedDarkAmount); - mBurnInOffsetY = MathUtils.lerp(0f, - getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */) - - mMaxBurnInOffsetY, - mInterpolatedDarkAmount); - - mFingerprintDrawable.setTint(ColorUtils.blendARGB(mLockScreenColor, - mAmbientDisplayColor, mInterpolatedDarkAmount)); - setStrokeWidth(MathUtils.lerp(DEFAULT_STROKE_WIDTH, DEFAULT_AOD_STROKE_WIDTH, - mInterpolatedDarkAmount)); - invalidateSelf(); - } - - @Override - public void dozeTimeTick() { - updateIcon(); - } - - @Override - public void draw(@NonNull Canvas canvas) { - if (isIlluminationShowing()) { - return; - } - canvas.save(); - canvas.translate(mBurnInOffsetX, mBurnInOffsetY); - mFingerprintDrawable.draw(canvas); - canvas.restore(); - } - - void animateHint() { - mHintAnimator.start(); - } - - void onDozeAmountChanged(float linear, float eased) { - mHintAnimator.cancel(); - mInterpolatedDarkAmount = eased; - updateIcon(); - } - - void setLockScreenColor(int color) { - if (mLockScreenColor == color) return; - mLockScreenColor = color; - updateIcon(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java index 804e2ab00bde..ec96af3ef500 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java @@ -16,6 +16,9 @@ package com.android.systemui.biometrics; +import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -23,7 +26,10 @@ import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.util.AttributeSet; +import android.util.MathUtils; import android.view.View; import android.widget.ImageView; @@ -34,12 +40,17 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.StatusBarState; +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieProperty; +import com.airbnb.lottie.model.KeyPath; /** * View corresponding with udfps_keyguard_view.xml */ public class UdfpsKeyguardView extends UdfpsAnimationView { - private final UdfpsKeyguardDrawable mFingerprintDrawable; - private ImageView mFingerprintView; + private UdfpsDrawable mFingerprintDrawable; // placeholder + private LottieAnimationView mAodFp; + private LottieAnimationView mLockScreenFp; + private int mUdfpsBouncerColor; private int mWallpaperTextColor; private int mStatusBarState; @@ -52,16 +63,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { private AnimatorSet mAnimatorSet; private int mAlpha; // 0-255 + // AOD anti-burn-in offsets + private final int mMaxBurnInOffsetX; + private final int mMaxBurnInOffsetY; + private float mBurnInOffsetX; + private float mBurnInOffsetY; + private float mBurnInProgress; + private float mInterpolatedDarkAmount; + + private ValueAnimator mHintAnimator; + public UdfpsKeyguardView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - mFingerprintDrawable = new UdfpsKeyguardDrawable(mContext); + mFingerprintDrawable = new UdfpsFpDrawable(context); + + mMaxBurnInOffsetX = context.getResources() + .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); + mMaxBurnInOffsetY = context.getResources() + .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mFingerprintView = findViewById(R.id.udfps_keyguard_animation_fp_view); - mFingerprintView.setForeground(mFingerprintDrawable); + mAodFp = findViewById(R.id.udfps_aod_fp); + mLockScreenFp = findViewById(R.id.udfps_lockscreen_fp); mBgProtection = findViewById(R.id.udfps_keyguard_fp_bg); @@ -69,7 +95,16 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { R.attr.wallpaperTextColorAccent); mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); + + // requires call to invalidate to update the color (see #updateColor) + mLockScreenFp.addValueCallback( + new KeyPath("**"), LottieProperty.COLOR_FILTER, + frameInfo -> new PorterDuffColorFilter(getColor(), PorterDuff.Mode.SRC_ATOP) + ); mUdfpsRequested = false; + + mHintAnimator = ObjectAnimator.ofFloat(mLockScreenFp, "progress", 1f, 0f, 1f); + mHintAnimator.setDuration(4000); } @Override @@ -89,10 +124,27 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { @Override public boolean dozeTimeTick() { - mFingerprintDrawable.dozeTimeTick(); + updateBurnInOffsets(); return true; } + private void updateBurnInOffsets() { + mBurnInOffsetX = MathUtils.lerp(0f, + getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */) + - mMaxBurnInOffsetX, mInterpolatedDarkAmount); + mBurnInOffsetY = MathUtils.lerp(0f, + getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */) + - mMaxBurnInOffsetY, mInterpolatedDarkAmount); + mBurnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount); + + mAodFp.setTranslationX(mBurnInOffsetX); + mAodFp.setTranslationY(mBurnInOffsetY); + mAodFp.setProgress(mBurnInProgress); + + mLockScreenFp.setTranslationX(mBurnInOffsetX); + mLockScreenFp.setTranslationY(mBurnInOffsetY); + } + void requestUdfps(boolean request, int color) { if (request) { mUdfpsRequestedColor = color; @@ -105,22 +157,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { void setStatusBarState(int statusBarState) { mStatusBarState = statusBarState; - updateColor(); } void updateColor() { - mFingerprintView.setAlpha(1f); - mFingerprintDrawable.setLockScreenColor(getColor()); + mLockScreenFp.invalidate(); + } + + private boolean showingUdfpsBouncer() { + return mBgProtection.getVisibility() == View.VISIBLE; } + private int getColor() { - if (mUdfpsRequested && mUdfpsRequestedColor != -1) { + if (isUdfpsColorRequested()) { return mUdfpsRequestedColor; + } else if (showingUdfpsBouncer()) { + return mUdfpsBouncerColor; } else { return mWallpaperTextColor; } } + private boolean isUdfpsColorRequested() { + return mUdfpsRequested && mUdfpsRequestedColor != -1; + } + /** * @param alpha between 0 and 255 */ @@ -130,6 +191,13 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { } @Override + protected int updateAlpha() { + int alpha = super.updateAlpha(); + mLockScreenFp.setImageAlpha(alpha); + return alpha; + } + + @Override int calculateAlpha() { if (mPauseAuth) { return 0; @@ -138,18 +206,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { } void onDozeAmountChanged(float linear, float eased) { - mFingerprintDrawable.onDozeAmountChanged(linear, eased); + mHintAnimator.cancel(); + mInterpolatedDarkAmount = eased; + updateBurnInOffsets(); + mLockScreenFp.setProgress(1f - mInterpolatedDarkAmount); + mAodFp.setAlpha(mInterpolatedDarkAmount); + + if (linear == 1f) { + mLockScreenFp.setVisibility(View.INVISIBLE); + } else { + mLockScreenFp.setVisibility(View.VISIBLE); + } } void animateHint() { - mFingerprintDrawable.animateHint(); + if (!isShadeLocked() && !mUdfpsRequested && mAlpha == 255 + && mLockScreenFp.isVisibleToUser()) { + mHintAnimator.start(); + } } /** * Animates in the bg protection circle behind the fp icon to highlight the icon. */ void animateUdfpsBouncer(Runnable onEndAnimation) { - if (mBgProtection.getVisibility() == View.VISIBLE && mBgProtection.getAlpha() == 1f) { + if (showingUdfpsBouncer() && mBgProtection.getAlpha() == 1f) { // already fully highlighted, don't re-animate return; } @@ -157,19 +238,6 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { if (mAnimatorSet != null) { mAnimatorSet.cancel(); } - ValueAnimator fpIconAnim; - if (isShadeLocked()) { - // set color and fade in since we weren't showing before - mFingerprintDrawable.setLockScreenColor(mTextColorPrimary); - fpIconAnim = ObjectAnimator.ofFloat(mFingerprintView, View.ALPHA, 0f, 1f); - } else { - // update icon color - fpIconAnim = new ValueAnimator(); - fpIconAnim.setIntValues(getColor(), mTextColorPrimary); - fpIconAnim.setEvaluator(new ArgbEvaluator()); - fpIconAnim.addUpdateListener(valueAnimator -> mFingerprintDrawable.setLockScreenColor( - (Integer) valueAnimator.getAnimatedValue())); - } mAnimatorSet = new AnimatorSet(); mAnimatorSet.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); @@ -181,11 +249,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { } }); + ValueAnimator fpIconColorAnim; + if (isShadeLocked()) { + // set color and fade in since we weren't showing before + mUdfpsBouncerColor = mTextColorPrimary; + fpIconColorAnim = ValueAnimator.ofInt(0, 255); + fpIconColorAnim.addUpdateListener(valueAnimator -> + mLockScreenFp.setImageAlpha((int) valueAnimator.getAnimatedValue())); + } else { + // update icon color + fpIconColorAnim = new ValueAnimator(); + fpIconColorAnim.setIntValues( + isUdfpsColorRequested() ? mUdfpsRequestedColor : mWallpaperTextColor, + mTextColorPrimary); + fpIconColorAnim.setEvaluator(ArgbEvaluator.getInstance()); + fpIconColorAnim.addUpdateListener(valueAnimator -> { + mUdfpsBouncerColor = (int) valueAnimator.getAnimatedValue(); + updateColor(); + }); + } + mAnimatorSet.playTogether( ObjectAnimator.ofFloat(mBgProtection, View.ALPHA, 0f, 1f), ObjectAnimator.ofFloat(mBgProtection, View.SCALE_X, 0f, 1f), ObjectAnimator.ofFloat(mBgProtection, View.SCALE_Y, 0f, 1f), - fpIconAnim); + fpIconColorAnim); mAnimatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -197,15 +285,11 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { mAnimatorSet.start(); } - private boolean isShadeLocked() { - return mStatusBarState == StatusBarState.SHADE_LOCKED; - } - /** * Animates out the bg protection circle behind the fp icon to unhighlight the icon. */ void animateAwayUdfpsBouncer(@Nullable Runnable onEndAnimation) { - if (mBgProtection.getVisibility() == View.GONE) { + if (!showingUdfpsBouncer()) { // already hidden return; } @@ -213,17 +297,25 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { if (mAnimatorSet != null) { mAnimatorSet.cancel(); } - ValueAnimator fpIconAnim; + + ValueAnimator fpIconColorAnim; if (isShadeLocked()) { // fade out - fpIconAnim = ObjectAnimator.ofFloat(mFingerprintView, View.ALPHA, 1f, 0f); + mUdfpsBouncerColor = mTextColorPrimary; + fpIconColorAnim = ValueAnimator.ofInt(255, 0); + fpIconColorAnim.addUpdateListener(valueAnimator -> + mLockScreenFp.setImageAlpha((int) valueAnimator.getAnimatedValue())); } else { // update icon color - fpIconAnim = new ValueAnimator(); - fpIconAnim.setIntValues(mTextColorPrimary, getColor()); - fpIconAnim.setEvaluator(new ArgbEvaluator()); - fpIconAnim.addUpdateListener(valueAnimator -> mFingerprintDrawable.setLockScreenColor( - (Integer) valueAnimator.getAnimatedValue())); + fpIconColorAnim = new ValueAnimator(); + fpIconColorAnim.setIntValues( + mTextColorPrimary, + isUdfpsColorRequested() ? mUdfpsRequestedColor : mWallpaperTextColor); + fpIconColorAnim.setEvaluator(ArgbEvaluator.getInstance()); + fpIconColorAnim.addUpdateListener(valueAnimator -> { + mUdfpsBouncerColor = (int) valueAnimator.getAnimatedValue(); + updateColor(); + }); } mAnimatorSet = new AnimatorSet(); @@ -231,7 +323,7 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { ObjectAnimator.ofFloat(mBgProtection, View.ALPHA, 1f, 0f), ObjectAnimator.ofFloat(mBgProtection, View.SCALE_X, 1f, 0f), ObjectAnimator.ofFloat(mBgProtection, View.SCALE_Y, 1f, 0f), - fpIconAnim); + fpIconColorAnim); mAnimatorSet.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mAnimatorSet.setDuration(500); @@ -244,10 +336,15 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { } } }); + mAnimatorSet.start(); } boolean isAnimating() { return mAnimatorSet != null && mAnimatorSet.isRunning(); } + + private boolean isShadeLocked() { + return mStatusBarState == StatusBarState.SHADE_LOCKED; + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index 35ca470df523..819de538c840 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -316,6 +316,14 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud } } + public void onBiometricError(int msgId, String errString, + BiometricSourceType biometricSourceType) { + if (biometricSourceType == BiometricSourceType.FACE) { + // show udfps hint when face auth fails + showHint(true); + } + } + public void onBiometricAuthenticated(int userId, BiometricSourceType biometricSourceType, boolean isStrongBiometric) { if (biometricSourceType == BiometricSourceType.FACE) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsSurfaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsSurfaceView.java index f8be35ab6cd8..77fad35d32d4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsSurfaceView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsSurfaceView.java @@ -23,43 +23,36 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.RectF; -import android.os.Build; -import android.os.UserHandle; -import android.provider.Settings; import android.util.AttributeSet; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; -import com.android.systemui.biometrics.UdfpsHbmTypes.HbmType; - /** - * Under-display fingerprint sensor Surface View. The surface should be used for HBM-specific things - * only. All other animations should be done on the other view. + * Surface View for providing the Global High-Brightness Mode (GHBM) illumination for UDFPS. */ -public class UdfpsSurfaceView extends SurfaceView implements UdfpsIlluminator { +public class UdfpsSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "UdfpsSurfaceView"; - private static final String SETTING_HBM_TYPE = - "com.android.systemui.biometrics.UdfpsSurfaceView.hbmType"; - private static final @HbmType int DEFAULT_HBM_TYPE = UdfpsHbmTypes.GLOBAL_HBM; /** - * This is used instead of {@link android.graphics.drawable.Drawable}, because the latter has - * several abstract methods that are not used here but require implementation. + * Notifies {@link UdfpsView} when to enable GHBM illumination. */ - private interface SimpleDrawable { - void draw(Canvas canvas); + interface GhbmIlluminationListener { + /** + * @param surface the surface for which GHBM should be enabled. + * @param onIlluminatedRunnable a runnable that should be run after GHBM is enabled. + */ + void enableGhbm(@NonNull Surface surface, @Nullable Runnable onIlluminatedRunnable); } @NonNull private final SurfaceHolder mHolder; @NonNull private final Paint mSensorPaint; - @NonNull private final SimpleDrawable mIlluminationDotDrawable; - private final int mOnIlluminatedDelayMs; - private final @HbmType int mHbmType; - @NonNull private RectF mSensorRect; - @Nullable private UdfpsHbmProvider mHbmProvider; + @Nullable private GhbmIlluminationListener mGhbmIlluminationListener; + @Nullable private Runnable mOnIlluminatedRunnable; + boolean mAwaitingSurfaceToStartIllumination; + boolean mHasValidSurface; public UdfpsSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); @@ -71,82 +64,77 @@ public class UdfpsSurfaceView extends SurfaceView implements UdfpsIlluminator { setZOrderOnTop(true); mHolder = getHolder(); + mHolder.addCallback(this); mHolder.setFormat(PixelFormat.RGBA_8888); - mSensorRect = new RectF(); mSensorPaint = new Paint(0 /* flags */); mSensorPaint.setAntiAlias(true); mSensorPaint.setARGB(255, 255, 255, 255); mSensorPaint.setStyle(Paint.Style.FILL); + } - mIlluminationDotDrawable = canvas -> { - canvas.drawOval(mSensorRect, mSensorPaint); - }; - - mOnIlluminatedDelayMs = mContext.getResources().getInteger( - com.android.internal.R.integer.config_udfps_illumination_transition_ms); - - if (Build.IS_ENG || Build.IS_USERDEBUG) { - mHbmType = Settings.Secure.getIntForUser(mContext.getContentResolver(), - SETTING_HBM_TYPE, DEFAULT_HBM_TYPE, UserHandle.USER_CURRENT); - } else { - mHbmType = DEFAULT_HBM_TYPE; + @Override public void surfaceCreated(SurfaceHolder holder) { + mHasValidSurface = true; + if (mAwaitingSurfaceToStartIllumination) { + doIlluminate(mOnIlluminatedRunnable); + mOnIlluminatedRunnable = null; + mAwaitingSurfaceToStartIllumination = false; } } @Override - public void setHbmProvider(@Nullable UdfpsHbmProvider hbmProvider) { - mHbmProvider = hbmProvider; + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Unused. } - @Override - public void startIllumination(@Nullable Runnable onIlluminatedRunnable) { - if (mHbmProvider != null) { - final Surface surface = - (mHbmType == UdfpsHbmTypes.GLOBAL_HBM) ? mHolder.getSurface() : null; - - final Runnable onHbmEnabled = () -> { - if (mHbmType == UdfpsHbmTypes.GLOBAL_HBM) { - drawImmediately(mIlluminationDotDrawable); - } - if (onIlluminatedRunnable != null) { - // No framework API can reliably tell when a frame reaches the panel. A timeout - // is the safest solution. - postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs); - } else { - Log.w(TAG, "startIllumination | onIlluminatedRunnable is null"); - } - }; - - mHbmProvider.enableHbm(mHbmType, surface, onHbmEnabled); - } else { - Log.e(TAG, "startIllumination | mHbmProvider is null"); - } + @Override public void surfaceDestroyed(SurfaceHolder holder) { + mHasValidSurface = false; } - @Override - public void stopIllumination() { - if (mHbmProvider != null) { - final Runnable onHbmDisabled = - (mHbmType == UdfpsHbmTypes.GLOBAL_HBM) ? this::invalidate : null; - mHbmProvider.disableHbm(onHbmDisabled); + void setGhbmIlluminationListener(@Nullable GhbmIlluminationListener listener) { + mGhbmIlluminationListener = listener; + } + + /** + * Note: there is no corresponding method to stop GHBM illumination. It is expected that + * {@link UdfpsView} will hide this view, which would destroy the surface and remove the + * illumination dot. + */ + void startGhbmIllumination(@Nullable Runnable onIlluminatedRunnable) { + if (mGhbmIlluminationListener == null) { + Log.e(TAG, "startIllumination | mGhbmIlluminationListener is null"); + return; + } + + if (mHasValidSurface) { + doIlluminate(onIlluminatedRunnable); } else { - Log.e(TAG, "stopIllumination | mHbmProvider is null"); + mAwaitingSurfaceToStartIllumination = true; + mOnIlluminatedRunnable = onIlluminatedRunnable; } } - void onSensorRectUpdated(@NonNull RectF sensorRect) { - mSensorRect = sensorRect; + private void doIlluminate(@Nullable Runnable onIlluminatedRunnable) { + if (mGhbmIlluminationListener == null) { + Log.e(TAG, "doIlluminate | mGhbmIlluminationListener is null"); + return; + } + + mGhbmIlluminationListener.enableGhbm(mHolder.getSurface(), onIlluminatedRunnable); } /** - * Immediately draws the provided drawable on this SurfaceView's surface. + * Immediately draws the illumination dot on this SurfaceView's surface. */ - private void drawImmediately(@NonNull SimpleDrawable drawable) { + void drawIlluminationDot(@NonNull RectF sensorRect) { + if (!mHasValidSurface) { + Log.e(TAG, "drawIlluminationDot | the surface is destroyed or was never created."); + return; + } Canvas canvas = null; try { canvas = mHolder.lockCanvas(); - drawable.draw(canvas); + canvas.drawOval(sensorRect, mSensorPaint); } finally { // Make sure the surface is never left in a bad state. if (canvas != null) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java index 5e5584cf30ea..15f77ffc08fd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java @@ -26,14 +26,19 @@ import android.graphics.Paint; import android.graphics.PointF; import android.graphics.RectF; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.os.Build; +import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; +import android.view.Surface; import android.view.View; import android.widget.FrameLayout; import com.android.systemui.R; +import com.android.systemui.biometrics.UdfpsHbmTypes.HbmType; import com.android.systemui.doze.DozeReceiver; /** @@ -43,18 +48,25 @@ import com.android.systemui.doze.DozeReceiver; public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIlluminator { private static final String TAG = "UdfpsView"; + private static final String SETTING_HBM_TYPE = + "com.android.systemui.biometrics.UdfpsSurfaceView.hbmType"; + private static final @HbmType int DEFAULT_HBM_TYPE = UdfpsHbmTypes.LOCAL_HBM; + private static final int DEBUG_TEXT_SIZE_PX = 32; @NonNull private final RectF mSensorRect; @NonNull private final Paint mDebugTextPaint; + private final float mSensorTouchAreaCoefficient; + private final int mOnIlluminatedDelayMs; + private final @HbmType int mHbmType; - @NonNull private UdfpsSurfaceView mHbmSurfaceView; + // Only used for UdfpsHbmTypes.GLOBAL_HBM. + @Nullable private UdfpsSurfaceView mGhbmView; + // Can be different for enrollment, BiometricPrompt, Keyguard, etc. @Nullable private UdfpsAnimationViewController mAnimationViewController; - // Used to obtain the sensor location. @NonNull private FingerprintSensorPropertiesInternal mSensorProps; - - private final float mSensorTouchAreaCoefficient; + @Nullable private UdfpsHbmProvider mHbmProvider; @Nullable private String mDebugMessage; private boolean mIlluminationRequested; @@ -81,7 +93,15 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin mDebugTextPaint.setColor(Color.BLUE); mDebugTextPaint.setTextSize(DEBUG_TEXT_SIZE_PX); - mIlluminationRequested = false; + mOnIlluminatedDelayMs = mContext.getResources().getInteger( + com.android.internal.R.integer.config_udfps_illumination_transition_ms); + + if (Build.IS_ENG || Build.IS_USERDEBUG) { + mHbmType = Settings.Secure.getIntForUser(mContext.getContentResolver(), + SETTING_HBM_TYPE, DEFAULT_HBM_TYPE, UserHandle.USER_CURRENT); + } else { + mHbmType = DEFAULT_HBM_TYPE; + } } // Don't propagate any touch events to the child views. @@ -93,7 +113,9 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin @Override protected void onFinishInflate() { - mHbmSurfaceView = findViewById(R.id.hbm_view); + if (mHbmType == UdfpsHbmTypes.GLOBAL_HBM) { + mGhbmView = findViewById(R.id.hbm_view); + } } void setSensorProperties(@NonNull FingerprintSensorPropertiesInternal properties) { @@ -102,7 +124,7 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin @Override public void setHbmProvider(@Nullable UdfpsHbmProvider hbmProvider) { - mHbmSurfaceView.setHbmProvider(hbmProvider); + mHbmProvider = hbmProvider; } @Override @@ -125,7 +147,6 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin 2 * mSensorProps.sensorRadius + paddingX, 2 * mSensorProps.sensorRadius + paddingY); - mHbmSurfaceView.onSensorRectUpdated(new RectF(mSensorRect)); if (mAnimationViewController != null) { mAnimationViewController.onSensorRectUpdated(new RectF(mSensorRect)); } @@ -204,8 +225,32 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin if (mAnimationViewController != null) { mAnimationViewController.onIlluminationStarting(); } - mHbmSurfaceView.setVisibility(View.VISIBLE); - mHbmSurfaceView.startIllumination(onIlluminatedRunnable); + + if (mGhbmView != null) { + mGhbmView.setGhbmIlluminationListener(this::doIlluminate); + mGhbmView.setVisibility(View.VISIBLE); + mGhbmView.startGhbmIllumination(onIlluminatedRunnable); + } else { + doIlluminate(null /* surface */, onIlluminatedRunnable); + } + } + + private void doIlluminate(@Nullable Surface surface, @Nullable Runnable onIlluminatedRunnable) { + if (mGhbmView != null && surface == null) { + Log.e(TAG, "doIlluminate | surface must be non-null for GHBM"); + } + mHbmProvider.enableHbm(mHbmType, surface, () -> { + if (mGhbmView != null) { + mGhbmView.drawIlluminationDot(mSensorRect); + } + if (onIlluminatedRunnable != null) { + // No framework API can reliably tell when a frame reaches the panel. A timeout + // is the safest solution. + postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs); + } else { + Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null"); + } + }); } @Override @@ -214,7 +259,10 @@ public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIllumin if (mAnimationViewController != null) { mAnimationViewController.onIlluminationStopped(); } - mHbmSurfaceView.setVisibility(View.INVISIBLE); - mHbmSurfaceView.stopIllumination(); + if (mGhbmView != null) { + mGhbmView.setGhbmIlluminationListener(null); + mGhbmView.setVisibility(View.INVISIBLE); + } + mHbmProvider.disableHbm(null /* onHbmDisabled */); } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index 020401ecd2f8..809e7a70d66c 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -25,6 +25,7 @@ import android.net.Uri; import android.os.Build; import android.util.IndentingPrintWriter; import android.util.Log; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; @@ -70,6 +71,7 @@ public class BrightLineFalsingManager implements FalsingManager { private final DoubleTapClassifier mDoubleTapClassifier; private final HistoryTracker mHistoryTracker; private final KeyguardStateController mKeyguardStateController; + private AccessibilityManager mAccessibilityManager; private final boolean mTestHarness; private final MetricsLogger mMetricsLogger; private int mIsFalseTouchCalls; @@ -175,6 +177,7 @@ public class BrightLineFalsingManager implements FalsingManager { @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, SingleTapClassifier singleTapClassifier, DoubleTapClassifier doubleTapClassifier, HistoryTracker historyTracker, KeyguardStateController keyguardStateController, + AccessibilityManager accessibilityManager, @TestHarness boolean testHarness) { mDataProvider = falsingDataProvider; mDockManager = dockManager; @@ -184,6 +187,7 @@ public class BrightLineFalsingManager implements FalsingManager { mDoubleTapClassifier = doubleTapClassifier; mHistoryTracker = historyTracker; mKeyguardStateController = keyguardStateController; + mAccessibilityManager = accessibilityManager; mTestHarness = testHarness; mDataProvider.addSessionListener(mSessionListener); @@ -328,7 +332,8 @@ public class BrightLineFalsingManager implements FalsingManager { || !mKeyguardStateController.isShowing() || mTestHarness || mDataProvider.isJustUnlockedWithFace() - || mDockManager.isDocked(); + || mDockManager.isDocked() + || mAccessibilityManager.isEnabled(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java index ffdcff2ef2b0..f18413be0134 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java @@ -41,7 +41,7 @@ public abstract class Classifier { public static final int SHADE_DRAG = 11; public static final int QS_COLLAPSE = 12; public static final int UDFPS_AUTHENTICATION = 13; - public static final int DISABLED_UDFPS_AFFORDANCE = 14; + public static final int LOCK_ICON = 14; public static final int QS_SWIPE = 15; public static final int BACK_GESTURE = 16; @@ -61,7 +61,7 @@ public abstract class Classifier { QS_COLLAPSE, BRIGHTNESS_SLIDER, UDFPS_AUTHENTICATION, - DISABLED_UDFPS_AFFORDANCE, + LOCK_ICON, QS_SWIPE, BACK_GESTURE }) diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java index 229801074246..d0fe1c37d4fa 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java @@ -155,7 +155,7 @@ class DistanceClassifier extends FalsingClassifier { || interactionType == SHADE_DRAG || interactionType == QS_COLLAPSE || interactionType == Classifier.UDFPS_AUTHENTICATION - || interactionType == Classifier.DISABLED_UDFPS_AFFORDANCE + || interactionType == Classifier.LOCK_ICON || interactionType == Classifier.QS_SWIPE) { return Result.passed(0); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java index c2ad7e6387d6..f040712706cd 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java @@ -46,7 +46,7 @@ public class TypeClassifier extends FalsingClassifier { @Classifier.InteractionType int interactionType, double historyBelief, double historyConfidence) { if (interactionType == Classifier.UDFPS_AUTHENTICATION - || interactionType == Classifier.DISABLED_UDFPS_AFFORDANCE) { + || interactionType == Classifier.LOCK_ICON) { return Result.passed(0); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index d85c9a718871..c97a30e6e13e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -77,6 +77,7 @@ import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; import com.android.systemui.statusbar.phone.ShadeController; @@ -227,6 +228,7 @@ public class DependencyProvider { Lazy<StatusBar> statusBarLazy, ShadeController shadeController, NotificationRemoteInputManager notificationRemoteInputManager, + NotificationShadeDepthController notificationShadeDepthController, SystemActions systemActions, @Main Handler mainHandler, UiEventLogger uiEventLogger, @@ -253,6 +255,7 @@ public class DependencyProvider { statusBarLazy, shadeController, notificationRemoteInputManager, + notificationShadeDepthController, systemActions, mainHandler, uiEventLogger, diff --git a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java index 60ee806d0a9f..735b3cd4b824 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java @@ -60,6 +60,13 @@ public class AlwaysOnDisplayPolicy { public int defaultDozeBrightness; /** + * Integer used to dim the screen just before the screen turns off. + * + * @see R.integer.config_screenBrightnessDim + */ + public int dimBrightness; + + /** * Integer array to map ambient brightness type to real screen brightness. * * @see Settings.Global#ALWAYS_ON_DISPLAY_CONSTANTS @@ -175,6 +182,8 @@ public class AlwaysOnDisplayPolicy { DEFAULT_WALLPAPER_VISIBILITY_MS); defaultDozeBrightness = resources.getInteger( com.android.internal.R.integer.config_screenBrightnessDoze); + dimBrightness = resources.getInteger( + com.android.internal.R.integer.config_screenBrightnessDim); screenBrightnessArray = mParser.getIntArray(KEY_SCREEN_BRIGHTNESS_ARRAY, resources.getIntArray( R.array.config_doze_brightness_sensor_to_brightness)); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index 92494cf5b546..470d2f364c1c 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -24,6 +24,7 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Handler; +import android.os.PowerManager; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -33,6 +34,8 @@ import android.view.Display; import com.android.systemui.doze.dagger.BrightnessSensor; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.doze.dagger.WrappedService; +import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.util.sensors.AsyncSensorManager; import java.io.PrintWriter; @@ -58,8 +61,11 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi private final Handler mHandler; private final SensorManager mSensorManager; private final Optional<Sensor> mLightSensorOptional; + private final WakefulnessLifecycle mWakefulnessLifecycle; + private final DozeParameters mDozeParameters; private final int[] mSensorToBrightness; private final int[] mSensorToScrimOpacity; + private final int mScreenBrightnessDim; private boolean mRegistered; private int mDefaultDozeBrightness; @@ -79,15 +85,20 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi public DozeScreenBrightness(Context context, @WrappedService DozeMachine.Service service, AsyncSensorManager sensorManager, @BrightnessSensor Optional<Sensor> lightSensorOptional, DozeHost host, Handler handler, - AlwaysOnDisplayPolicy alwaysOnDisplayPolicy) { + AlwaysOnDisplayPolicy alwaysOnDisplayPolicy, + WakefulnessLifecycle wakefulnessLifecycle, + DozeParameters dozeParameters) { mContext = context; mDozeService = service; mSensorManager = sensorManager; mLightSensorOptional = lightSensorOptional; + mWakefulnessLifecycle = wakefulnessLifecycle; + mDozeParameters = dozeParameters; mDozeHost = host; mHandler = handler; mDefaultDozeBrightness = alwaysOnDisplayPolicy.defaultDozeBrightness; + mScreenBrightnessDim = alwaysOnDisplayPolicy.dimBrightness; mSensorToBrightness = alwaysOnDisplayPolicy.screenBrightnessArray; mSensorToScrimOpacity = alwaysOnDisplayPolicy.dimmingScrimArray; } @@ -178,7 +189,9 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi } private void resetBrightnessToDefault() { - mDozeService.setDozeScreenBrightness(clampToUserSetting(mDefaultDozeBrightness)); + mDozeService.setDozeScreenBrightness( + clampToDimBrightnessForScreenOff( + clampToUserSetting(mDefaultDozeBrightness))); mDozeHost.setAodDimmingScrim(0f); } //TODO: brightnessfloat change usages to float. @@ -189,6 +202,21 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi return Math.min(brightness, userSetting); } + /** + * Clamp the brightness to the dim brightness value used by PowerManagerService just before the + * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we + * don't raise the brightness back to the user setting before playing the screen off animation. + */ + private int clampToDimBrightnessForScreenOff(int brightness) { + if (mDozeParameters.shouldControlUnlockedScreenOff() + && mWakefulnessLifecycle.getLastSleepReason() + == PowerManager.GO_TO_SLEEP_REASON_TIMEOUT) { + return Math.min(mScreenBrightnessDim, brightness); + } else { + return brightness; + } + } + private void setLightSensorEnabled(boolean enabled) { if (enabled && !mRegistered && mLightSensorOptional.isPresent()) { // Wait until we get an event from the sensor until indicating ready. diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt index 73abf4519f73..15e3f3a6b1e9 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt @@ -22,6 +22,7 @@ private const val MILLIS_PER_MINUTES = 1000 * 60f private const val BURN_IN_PREVENTION_PERIOD_Y = 521f private const val BURN_IN_PREVENTION_PERIOD_X = 83f private const val BURN_IN_PREVENTION_PERIOD_SCALE = 180f +private const val BURN_IN_PREVENTION_PERIOD_PROGRESS = 120f /** * Returns the translation offset that should be used to avoid burn in at @@ -37,6 +38,15 @@ fun getBurnInOffset(amplitude: Int, xAxis: Boolean): Int { } /** + * Returns a progress offset (between 0f and 1.0f) that should be used to avoid burn in at + * the current time. + */ +fun getBurnInProgressOffset(): Float { + return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES, + 1f, BURN_IN_PREVENTION_PERIOD_PROGRESS) +} + +/** * Returns a value to scale a view in order to avoid burn in. */ fun getBurnInScale(): Float { diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index bb44b09f1bce..bc4ced452630 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -71,7 +71,6 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; -import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -103,7 +102,6 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite private final LockPatternUtils mLockPatternUtils; private final KeyguardStateController mKeyguardStateController; - private final NotificationShadeDepthController mDepthController; private final SysUiState mSysUiState; private final ActivityStarter mActivityStarter; private final SysuiColorExtractor mSysuiColorExtractor; @@ -164,7 +162,6 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite IActivityManager iActivityManager, @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger, - NotificationShadeDepthController depthController, SysuiColorExtractor colorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, @@ -196,7 +193,6 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite iActivityManager, telecomManager, metricsLogger, - depthController, colorExtractor, statusBarService, notificationShadeWindowController, @@ -212,7 +208,6 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite mLockPatternUtils = lockPatternUtils; mKeyguardStateController = keyguardStateController; - mDepthController = depthController; mSysuiColorExtractor = colorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; @@ -267,9 +262,8 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite protected ActionsDialogLite createDialog() { initDialogItems(); - mDepthController.setShowingHomeControls(true); ActionsDialog dialog = new ActionsDialog(getContext(), mAdapter, mOverflowAdapter, - this::getWalletViewController, mDepthController, mSysuiColorExtractor, + this::getWalletViewController, mSysuiColorExtractor, mStatusBarService, mNotificationShadeWindowController, mSysUiState, this::onRotate, isKeyguardShowing(), mPowerAdapter, getEventLogger(), getStatusBar()); @@ -336,16 +330,15 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter, Provider<GlobalActionsPanelPlugin.PanelViewController> walletFactory, - NotificationShadeDepthController depthController, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing, MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger, StatusBar statusBar) { super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions, - adapter, overflowAdapter, depthController, sysuiColorExtractor, - statusBarService, notificationShadeWindowController, sysuiState, - onRotateCallback, keyguardShowing, powerAdapter, uiEventLogger, null, + adapter, overflowAdapter, sysuiColorExtractor, statusBarService, + notificationShadeWindowController, sysuiState, onRotateCallback, + keyguardShowing, powerAdapter, uiEventLogger, null, statusBar); mWalletFactory = walletFactory; @@ -494,8 +487,6 @@ public class GlobalActionsDialog extends GlobalActionsDialogLite float animatedValue = animation.getAnimatedFraction(); int alpha = (int) (animatedValue * mScrimAlpha * 255); mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, - mGlobalActionsLayout); }); ObjectAnimator xAnimator = diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index f30d6b13ad02..5acb3038b91b 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -119,7 +119,6 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; import com.android.systemui.scrim.ScrimDrawable; -import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -190,7 +189,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final TelecomManager mTelecomManager; private final MetricsLogger mMetricsLogger; private final UiEventLogger mUiEventLogger; - private final NotificationShadeDepthController mDepthController; private final SysUiState mSysUiState; private final GlobalActionsInfoProvider mInfoProvider; @@ -329,7 +327,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene IActivityManager iActivityManager, @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger, - NotificationShadeDepthController depthController, SysuiColorExtractor colorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, @@ -362,7 +359,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMetricsLogger = metricsLogger; mUiEventLogger = uiEventLogger; mInfoProvider = infoProvider; - mDepthController = depthController; mSysuiColorExtractor = colorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; @@ -652,11 +648,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene protected ActionsDialogLite createDialog() { initDialogItems(); - mDepthController.setShowingHomeControls(false); ActionsDialogLite dialog = new ActionsDialogLite(mContext, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActionsLite, - mAdapter, mOverflowAdapter, - mDepthController, mSysuiColorExtractor, + mAdapter, mOverflowAdapter, mSysuiColorExtractor, mStatusBarService, mNotificationShadeWindowController, mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter, mUiEventLogger, mInfoProvider, mStatusBar); @@ -2125,7 +2119,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene protected boolean mShowing; protected float mScrimAlpha; protected final NotificationShadeWindowController mNotificationShadeWindowController; - protected final NotificationShadeDepthController mDepthController; protected final SysUiState mSysUiState; private ListPopupWindow mOverflowPopup; private Dialog mPowerOptionsDialog; @@ -2181,7 +2174,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene ActionsDialogLite(Context context, int themeRes, MyAdapter adapter, MyOverflowAdapter overflowAdapter, - NotificationShadeDepthController depthController, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing, @@ -2192,7 +2184,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mAdapter = adapter; mOverflowAdapter = overflowAdapter; mPowerOptionsAdapter = powerAdapter; - mDepthController = depthController; mColorExtractor = sysuiColorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; @@ -2409,7 +2400,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene float animatedValue = animation.getAnimatedFraction(); int alpha = (int) (animatedValue * mScrimAlpha * 255); mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, mGlobalActionsLayout); }); ObjectAnimator xAnimator = @@ -2439,7 +2429,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene float animatedValue = 1f - animation.getAnimatedFraction(); int alpha = (int) (animatedValue * mScrimAlpha * 255); mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, mGlobalActionsLayout); }); float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); @@ -2476,7 +2465,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene dismissOverflow(true); dismissPowerOptions(true); mNotificationShadeWindowController.setRequestTopUi(false, TAG); - mDepthController.updateGlobalDialogVisibility(0, null /* view */); mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, false) .commitUpdate(mContext.getDisplayId()); super.dismiss(); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java index e37d3d586ccc..a641ad4b338b 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java @@ -30,7 +30,6 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.android.internal.R; -import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.Utils; import com.android.systemui.plugins.GlobalActions; import com.android.systemui.scrim.ScrimDrawable; @@ -51,7 +50,6 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks private final KeyguardStateController mKeyguardStateController; private final DeviceProvisionedController mDeviceProvisionedController; private final BlurUtils mBlurUtils; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final CommandQueue mCommandQueue; private GlobalActionsDialogLite mGlobalActionsDialog; private boolean mDisabled; @@ -60,15 +58,13 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks public GlobalActionsImpl(Context context, CommandQueue commandQueue, Lazy<GlobalActionsDialogLite> globalActionsDialogLazy, BlurUtils blurUtils, KeyguardStateController keyguardStateController, - DeviceProvisionedController deviceProvisionedController, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + DeviceProvisionedController deviceProvisionedController) { mContext = context; mGlobalActionsDialogLazy = globalActionsDialogLazy; mKeyguardStateController = keyguardStateController; mDeviceProvisionedController = deviceProvisionedController; mCommandQueue = commandQueue; mBlurUtils = blurUtils; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; mCommandQueue.addCallback(this); } @@ -87,7 +83,6 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks mGlobalActionsDialog = mGlobalActionsDialogLazy.get(); mGlobalActionsDialog.showOrHideDialog(mKeyguardStateController.isShowing(), mDeviceProvisionedController.isDeviceProvisioned()); - mKeyguardUpdateMonitor.requestFaceAuth(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt index 17b532a643cd..25837e3aacdf 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt @@ -24,7 +24,6 @@ import android.service.quickaccesswallet.QuickAccessWalletClient import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.ImageView import android.widget.TextView import com.android.systemui.R import com.android.systemui.controls.controller.ControlsController @@ -76,8 +75,7 @@ class GlobalActionsInfoProvider @Inject constructor( val message = view.findViewById<TextView>(R.id.global_actions_change_message) message?.setText(context.getString(R.string.global_actions_change_description, walletTitle)) - val button = view.findViewById<ImageView>(R.id.global_actions_change_button) - button.setOnClickListener { _ -> + view.setOnClickListener { _ -> dismissParent.run() activityStarter.postStartActivityDismissingKeyguard(pendingIntent) } @@ -119,4 +117,4 @@ class GlobalActionsInfoProvider @Inject constructor( val count = sharedPrefs.getInt(KEY_VIEW_COUNT, 0) sharedPrefs.edit().putInt(KEY_VIEW_COUNT, count + 1).apply() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPopupMenu.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPopupMenu.java index ac4fc62bf1c9..d1a103e3a8fa 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPopupMenu.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsPopupMenu.java @@ -22,7 +22,6 @@ import android.content.res.Resources; import android.util.LayoutDirection; import android.view.View; import android.view.View.MeasureSpec; -import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.ListPopupWindow; @@ -49,11 +48,9 @@ public class GlobalActionsPopupMenu extends ListPopupWindow { mContext = context; Resources res = mContext.getResources(); setBackgroundDrawable( - res.getDrawable(R.drawable.rounded_bg_full, context.getTheme())); + res.getDrawable(R.drawable.global_actions_popup_bg, context.getTheme())); mIsDropDownMode = isDropDownMode; - // required to show above the global actions dialog - setWindowLayoutType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); setInputMethodMode(INPUT_METHOD_NOT_NEEDED); setModal(true); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java index 2873cd36409d..9b83b75cec22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java @@ -40,18 +40,22 @@ public class KeyguardIndication { private final View.OnClickListener mOnClickListener; @Nullable private final Drawable mBackground; + @Nullable + private final Long mMinVisibilityMillis; // in milliseconds private KeyguardIndication( CharSequence message, ColorStateList textColor, Drawable icon, View.OnClickListener onClickListener, - Drawable background) { + Drawable background, + Long minVisibilityMillis) { mMessage = message; mTextColor = textColor; mIcon = icon; mOnClickListener = onClickListener; mBackground = background; + mMinVisibilityMillis = minVisibilityMillis; } /** @@ -89,6 +93,14 @@ public class KeyguardIndication { return mBackground; } + /** + * Minimum time to show text in milliseconds. + * @return null if unspecified + */ + public @Nullable Long getMinVisibilityMillis() { + return mMinVisibilityMillis; + } + @Override public String toString() { String str = "KeyguardIndication{"; @@ -96,6 +108,7 @@ public class KeyguardIndication { if (mIcon != null) str += " mIcon=" + mIcon; if (mOnClickListener != null) str += " mOnClickListener=" + mOnClickListener; if (mBackground != null) str += " mBackground=" + mBackground; + if (mMinVisibilityMillis != null) str += " mMinVisibilityMillis=" + mMinVisibilityMillis; str += "}"; return str; } @@ -109,6 +122,7 @@ public class KeyguardIndication { private View.OnClickListener mOnClickListener; private ColorStateList mTextColor; private Drawable mBackground; + private Long mMinVisibilityMillis; public Builder() { } @@ -155,6 +169,15 @@ public class KeyguardIndication { } /** + * Optional. Set a required minimum visibility time in milliseconds for the text + * to show. + */ + public Builder setMinVisibilityMillis(Long minVisibilityMillis) { + this.mMinVisibilityMillis = minVisibilityMillis; + return this; + } + + /** * Build the KeyguardIndication. */ public KeyguardIndication build() { @@ -166,7 +189,8 @@ public class KeyguardIndication { } return new KeyguardIndication( - mMessage, mTextColor, mIcon, mOnClickListener, mBackground); + mMessage, mTextColor, mIcon, mOnClickListener, mBackground, + mMinVisibilityMillis); } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java index fc5f3b8ae994..2d215e0f1f62 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java @@ -23,7 +23,6 @@ import android.text.TextUtils; import androidx.annotation.IntDef; -import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -164,15 +163,14 @@ public class KeyguardIndicationRotateTextViewController extends * Transient messages: * - show immediately * - will continue to be in the rotation of messages shown until hideTransient is called. - * - can be presented with an "error" color if isError is true */ - public void showTransient(CharSequence newIndication, boolean isError) { + public void showTransient(CharSequence newIndication) { + final long inAnimationDuration = 600L; // see KeyguardIndicationTextView.getYInDuration updateIndication(INDICATION_TYPE_TRANSIENT, new KeyguardIndication.Builder() .setMessage(newIndication) - .setTextColor(isError - ? Utils.getColorError(getContext()) - : mInitialTextColorState) + .setMinVisibilityMillis(2000L + inAnimationDuration) + .setTextColor(mInitialTextColorState) .build(), /* showImmediately */true); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 70837385b858..62b92cb33f5c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -73,7 +73,7 @@ public class KeyguardService extends Service { "persist.wm.enable_remote_keyguard_animation"; private static final int sEnableRemoteKeyguardAnimation = - SystemProperties.getInt(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, 1); + SystemProperties.getInt(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, 0); /** * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 38d153e38ca9..941f2c6f4282 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -31,6 +31,7 @@ import com.android.keyguard.KeyguardViewController import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController +import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject @@ -89,7 +90,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private val keyguardStateController: KeyguardStateController, private val keyguardViewMediator: Lazy<KeyguardViewMediator>, private val keyguardViewController: KeyguardViewController, - private val smartspaceTransitionController: SmartspaceTransitionController + private val smartspaceTransitionController: SmartspaceTransitionController, + private val featureFlags: FeatureFlags ) : KeyguardStateController.Callback { /** @@ -346,6 +348,10 @@ class KeyguardUnlockAnimationController @Inject constructor( * keyguard visible. */ private fun updateKeyguardViewMediatorIfThresholdsReached() { + if (!featureFlags.isNewKeyguardSwipeAnimationEnabled) { + return + } + val dismissAmount = keyguardStateController.dismissAmount // Hide the keyguard if we're fully dismissed, or if we're swiping to dismiss and have @@ -382,6 +388,10 @@ class KeyguardUnlockAnimationController @Inject constructor( * know if it needs to do something as a result. */ private fun updateSmartSpaceTransition() { + if (!featureFlags.isSmartSpaceSharedElementTransitionEnabled) { + return + } + val dismissAmount = keyguardStateController.dismissAmount // If we've begun a swipe, and are capable of doing the SmartSpace transition, start it! diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 9f0f3fb1b899..8a9fc2685a71 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -176,7 +176,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000; - private static final boolean DEBUG = true; + private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final boolean DEBUG_SIM_STATES = KeyguardConstants.DEBUG_SIM_STATES; private final static String TAG = "KeyguardViewMediator"; @@ -1001,12 +1001,6 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, mPowerGestureIntercepted = false; mGoingToSleep = true; - // Reset keyguard going away state so we can start listening for fingerprint. We - // explicitly DO NOT want to call - // mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false) - // here, since that will mess with the device lock state. - mUpdateMonitor.dispatchKeyguardGoingAway(false); - // Lock immediately based on setting if secure (user has a pin/pattern/password). // This also "locks" the device when not secure to provide easy access to the // camera while preventing unwanted input. @@ -1044,7 +1038,15 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, playSounds(true); } } + mUpdateMonitor.dispatchStartedGoingToSleep(offReason); + + // Reset keyguard going away state so we can start listening for fingerprint. We + // explicitly DO NOT want to call + // mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false) + // here, since that will mess with the device lock state. + mUpdateMonitor.dispatchKeyguardGoingAway(false); + notifyStartedGoingToSleep(); } @@ -1707,8 +1709,8 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, * Disable notification shade background blurs until the keyguard is dismissed. * (Used during app launch animations) */ - public void disableBlursUntilHidden() { - mNotificationShadeDepthController.get().setIgnoreShadeBlurUntilHidden(true); + public void setBlursDisabledForAppLaunch(boolean disabled) { + mNotificationShadeDepthController.get().setBlursDisabledForAppLaunch(disabled); } public boolean isSecure() { @@ -2191,6 +2193,15 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, if (!mHiding && !mSurfaceBehindRemoteAnimationRequested && !mKeyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture()) { + if (finishedCallback != null) { + // There will not execute animation, send a finish callback to ensure the remote + // animation won't hanging there. + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onAnimationFinished", e); + } + } setShowingLocked(mShowing, true /* force */); return; } @@ -2208,12 +2219,6 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, mDrawnCallback = null; } - // only play "unlock" noises if not on a call (since the incall UI - // disables the keyguard) - if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) { - playSounds(false); - } - LatencyTracker.getInstance(mContext) .onActionEnd(LatencyTracker.ACTION_LOCKSCREEN_UNLOCK); @@ -2331,6 +2336,13 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, } private void onKeyguardExitFinished() { + // only play "unlock" noises if not on a call (since the incall UI + // disables the keyguard) + if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) { + Log.i("TEST", "playSounds: false"); + playSounds(false); + } + setShowingLocked(false); mWakeAndUnlocking = false; mDismissCallbackRegistry.notifyDismissSucceeded(); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 3957a60c5b45..2facf3dedd18 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -12,10 +12,12 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator @@ -26,6 +28,9 @@ import com.android.systemui.util.Utils import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock +import java.io.FileDescriptor +import java.io.PrintWriter import java.util.TreeMap import javax.inject.Inject import javax.inject.Provider @@ -45,12 +50,14 @@ class MediaCarouselController @Inject constructor( private val visualStabilityManager: VisualStabilityManager, private val mediaHostStatesManager: MediaHostStatesManager, private val activityStarter: ActivityStarter, + private val systemClock: SystemClock, @Main executor: DelayableExecutor, private val mediaManager: MediaDataManager, configurationController: ConfigurationController, falsingCollector: FalsingCollector, - falsingManager: FalsingManager -) { + falsingManager: FalsingManager, + dumpManager: DumpManager +) : Dumpable { /** * The current width of the carousel */ @@ -164,6 +171,7 @@ class MediaCarouselController @Inject constructor( lateinit var updateUserVisibility: () -> Unit init { + dumpManager.registerDumpable(TAG, this) mediaFrame = inflateMediaCarousel() mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) @@ -204,7 +212,7 @@ class MediaCarouselController @Inject constructor( isSsReactivated: Boolean ) { if (addOrUpdatePlayer(key, oldKey, data)) { - MediaPlayerData.getMediaPlayer(key, null)?.let { + MediaPlayerData.getMediaPlayer(key)?.let { logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED it.mInstanceId, /* isRecommendationCard */ false, @@ -241,7 +249,7 @@ class MediaCarouselController @Inject constructor( if (DEBUG) Log.d(TAG, "Loading Smartspace media update") if (data.isActive) { addSmartspaceMediaRecommendations(key, data, shouldPrioritize) - MediaPlayerData.getMediaPlayer(key, null)?.let { + MediaPlayerData.getMediaPlayer(key)?.let { logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED it.mInstanceId, /* isRecommendationCard */ true, @@ -344,7 +352,8 @@ class MediaCarouselController @Inject constructor( // Returns true if new player is added private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData): Boolean { val dataCopy = data.copy(backgroundColor = bgColor) - val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey) + MediaPlayerData.moveIfExists(oldKey, key) + val existingPlayer = MediaPlayerData.getMediaPlayer(key) val curVisibleMediaKey = MediaPlayerData.playerKeys() .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) if (existingPlayer == null) { @@ -357,12 +366,12 @@ class MediaCarouselController @Inject constructor( newPlayer.playerViewHolder?.player?.setLayoutParams(lp) newPlayer.bindPlayer(dataCopy, key) newPlayer.setListening(currentlyExpanded) - MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer) + MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer, systemClock) updatePlayerToState(newPlayer, noAnimation = true) reorderAllPlayers(curVisibleMediaKey) } else { existingPlayer.bindPlayer(dataCopy, key) - MediaPlayerData.addMediaPlayer(key, dataCopy, existingPlayer) + MediaPlayerData.addMediaPlayer(key, dataCopy, existingPlayer, systemClock) if (visualStabilityManager.isReorderingAllowed || shouldScrollToActivePlayer) { reorderAllPlayers(curVisibleMediaKey) } else { @@ -386,7 +395,7 @@ class MediaCarouselController @Inject constructor( shouldPrioritize: Boolean ) { if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") - if (MediaPlayerData.getMediaPlayer(key, null) != null) { + if (MediaPlayerData.getMediaPlayer(key) != null) { Log.w(TAG, "Skip adding smartspace target in carousel") return } @@ -406,7 +415,7 @@ class MediaCarouselController @Inject constructor( newRecs.bindRecommendation(data.copy(backgroundColor = bgColor)) val curVisibleMediaKey = MediaPlayerData.playerKeys() .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize) + MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize, systemClock) updatePlayerToState(newRecs, noAnimation = true) reorderAllPlayers(curVisibleMediaKey) updatePageIndicator() @@ -418,7 +427,7 @@ class MediaCarouselController @Inject constructor( } } - private fun removePlayer( + fun removePlayer( key: String, dismissMediaData: Boolean = true, dismissRecommendation: Boolean = true @@ -745,6 +754,15 @@ class MediaCarouselController @Inject constructor( } mediaManager.onSwipeToDismiss() } + + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("keysNeedRemoval: $keysNeedRemoval") + println("playerKeys: ${MediaPlayerData.playerKeys()}") + println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") + println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") + } + } } @VisibleForTesting @@ -774,9 +792,9 @@ internal object MediaPlayerData { private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() - fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) { + fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel, clock: SystemClock) { removeMediaPlayer(key) - val sortKey = MediaSortKey(isSsMediaRec = false, data, System.currentTimeMillis()) + val sortKey = MediaSortKey(isSsMediaRec = false, data, clock.currentTimeMillis()) mediaData.put(key, sortKey) mediaPlayers.put(sortKey, player) } @@ -785,23 +803,29 @@ internal object MediaPlayerData { key: String, data: SmartspaceMediaData, player: MediaControlPanel, - shouldPrioritize: Boolean + shouldPrioritize: Boolean, + clock: SystemClock ) { shouldPrioritizeSs = shouldPrioritize removeMediaPlayer(key) - val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY, System.currentTimeMillis()) + val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY, clock.currentTimeMillis()) mediaData.put(key, sortKey) mediaPlayers.put(sortKey, player) smartspaceMediaData = data } - fun getMediaPlayer(key: String, oldKey: String?): MediaControlPanel? { - // If the key was changed, update entry - oldKey?.let { - if (it != key) { - mediaData.remove(it)?.let { sortKey -> mediaData.put(key, sortKey) } - } + fun moveIfExists(oldKey: String?, newKey: String) { + if (oldKey == null || oldKey == newKey) { + return + } + + mediaData.remove(oldKey)?.let { + removeMediaPlayer(newKey) + mediaData.put(newKey, it) } + } + + fun getMediaPlayer(key: String): MediaControlPanel? { return mediaData.get(key)?.let { mediaPlayers.get(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index b0d4cb1c9818..cbcec9531ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -236,7 +236,7 @@ class MediaCarouselScrollHandler( } private fun updateSettingsPresentation() { - if (showsSettingsButton) { + if (showsSettingsButton && settingsButton.width > 0) { val settingsOffset = MathUtils.map( 0.0f, getMaxTranslation().toFloat(), diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 19a67e95a496..902e8c28a85d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -347,10 +347,11 @@ public class MediaControlPanel { artistText.setText(data.getArtist()); // Transfer chip - mPlayerViewHolder.getSeamless().setVisibility(View.VISIBLE); + ViewGroup seamlessView = mPlayerViewHolder.getSeamless(); + seamlessView.setVisibility(View.VISIBLE); setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); - mPlayerViewHolder.getSeamless().setOnClickListener(v -> { + seamlessView.setOnClickListener(v -> { mMediaOutputDialogFactory.create(data.getPackageName(), true); }); @@ -374,9 +375,9 @@ public class MediaControlPanel { collapsedSet.setAlpha(seamlessId, seamlessAlpha); // Disable clicking on output switcher for resumption controls. mPlayerViewHolder.getSeamless().setEnabled(!data.getResumption()); + String deviceString = null; if (showFallback) { iconView.setImageDrawable(null); - deviceName.setText(null); } else if (device != null) { Drawable icon = device.getIcon(); iconView.setVisibility(View.VISIBLE); @@ -387,13 +388,16 @@ public class MediaControlPanel { } else { iconView.setImageDrawable(icon); } - deviceName.setText(device.getName()); + deviceString = device.getName(); } else { // Reset to default Log.w(TAG, "device is null. Not binding output chip."); iconView.setVisibility(View.GONE); - deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); + deviceString = mContext.getString( + com.android.internal.R.string.ext_media_seamless_action); } + deviceName.setText(deviceString); + seamlessView.setContentDescription(deviceString); List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); // Media controls @@ -451,8 +455,12 @@ public class MediaControlPanel { if (mKey != null) { closeGuts(); - mMediaDataManagerLazy.get().dismissMediaData(mKey, - MediaViewController.GUTS_ANIMATION_DURATION + 100); + if (!mMediaDataManagerLazy.get().dismissMediaData(mKey, + MediaViewController.GUTS_ANIMATION_DURATION + 100)) { + Log.w(TAG, "Manager failed to dismiss media " + mKey); + // Remove directly from carousel to let user recover - TODO(b/190799184) + mMediaCarouselController.removePlayer(key, false, false); + } } else { Log.w(TAG, "Dismiss media with null notification. Token uid=" + data.getToken().getUid()); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index df1b07f3e0b2..0a28b47923da 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -430,7 +430,11 @@ class MediaDataManager( notifyMediaDataRemoved(key) } - fun dismissMediaData(key: String, delay: Long) { + /** + * Dismiss a media entry. Returns false if the key was not found. + */ + fun dismissMediaData(key: String, delay: Long): Boolean { + val existed = mediaEntries[key] != null backgroundExecutor.execute { mediaEntries[key]?.let { mediaData -> if (mediaData.isLocalSession) { @@ -442,6 +446,7 @@ class MediaDataManager( } } foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + return existed } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 186f961ff1b6..fb601e310702 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -537,8 +537,19 @@ class MediaHierarchyManager @Inject constructor( ) { val desiredLocation = calculateLocation() if (desiredLocation != this.desiredLocation || forceStateUpdate) { - if (this.desiredLocation >= 0) { + if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { + // Only update previous location when it actually changes previousLocation = this.desiredLocation + } else if (forceStateUpdate) { + val onLockscreen = (!bypassController.bypassEnabled && + (statusbarState == StatusBarState.KEYGUARD || + statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) + if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN && + !onLockscreen) { + // If media active state changed and the device is now unlocked, update the + // previous location so we animate between the correct hosts + previousLocation = LOCATION_QQS + } } val isNewView = this.desiredLocation == -1 this.desiredLocation = desiredLocation diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 0d5faff65aab..391dff634dab 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -46,7 +46,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private ViewGroup mConnectedItem; - private boolean mInclueDynamicGroup; + private boolean mIncludeDynamicGroup; public MediaOutputAdapter(MediaOutputController controller) { super(controller); @@ -56,7 +56,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { super.onCreateViewHolder(viewGroup, viewType); - return new MediaDeviceViewHolder(mHolderView); } @@ -66,7 +65,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { if (position == size && mController.isZeroMode()) { viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */, true /* bottomMargin */); - } else if (mInclueDynamicGroup) { + } else if (mIncludeDynamicGroup) { if (position == 0) { viewHolder.onBind(CUSTOMIZED_ITEM_DYNAMIC_GROUP, true /* topMargin */, false /* bottomMargin */); @@ -76,11 +75,12 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // from "position - 1". viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())) .get(position - 1), - false /* topMargin */, position == size /* bottomMargin */); + false /* topMargin */, position == size /* bottomMargin */, position); } } else if (position < size) { viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position), - position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */); + position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */, + position); } else if (DEBUG) { Log.d(TAG, "Incorrect position: " + position); } @@ -88,8 +88,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @Override public int getItemCount() { - mInclueDynamicGroup = mController.getSelectedMediaDevice().size() > 1; - if (mController.isZeroMode() || mInclueDynamicGroup) { + mIncludeDynamicGroup = mController.getSelectedMediaDevice().size() > 1; + if (mController.isZeroMode() || mIncludeDynamicGroup) { // Add extra one for "pair new" or dynamic group return mController.getMediaDevices().size() + 1; } @@ -120,15 +120,17 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } @Override - void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { - super.onBind(device, topMargin, bottomMargin); - final boolean currentlyConnected = !mInclueDynamicGroup && isCurrentlyConnected(device); + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) { + super.onBind(device, topMargin, bottomMargin, position); + final boolean currentlyConnected = !mIncludeDynamicGroup + && isCurrentlyConnected(device); if (currentlyConnected) { mConnectedItem = mContainerLayout; } mBottomDivider.setVisibility(View.GONE); mCheckBox.setVisibility(View.GONE); - if (currentlyConnected && mController.isActiveRemoteDevice(device)) { + if (currentlyConnected && mController.isActiveRemoteDevice(device) + && mController.getSelectableMediaDevice().size() > 0) { // Init active device layout mDivider.setVisibility(View.VISIBLE); mDivider.setTransitionAlpha(1); @@ -140,6 +142,9 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mDivider.setVisibility(View.GONE); mAddIcon.setVisibility(View.GONE); } + if (mCurrentActivePosition == position) { + mCurrentActivePosition = -1; + } if (mController.isTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING && !mController.hasAdjustVolumeUserRestriction()) { @@ -160,6 +165,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { setTwoLineLayout(device, true /* bFocused */, true /* showSeekBar */, false /* showProgressBar */, false /* showSubtitle */); initSeekbar(device); + mCurrentActivePosition = position; } else { setSingleLineLayout(getItemTitle(device), false /* bFocused */); mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); @@ -186,11 +192,16 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mConnectedItem = mContainerLayout; mBottomDivider.setVisibility(View.GONE); mCheckBox.setVisibility(View.GONE); - mDivider.setVisibility(View.VISIBLE); - mDivider.setTransitionAlpha(1); - mAddIcon.setVisibility(View.VISIBLE); - mAddIcon.setTransitionAlpha(1); - mAddIcon.setOnClickListener(v -> onEndItemClick()); + if (mController.getSelectableMediaDevice().size() > 0) { + mDivider.setVisibility(View.VISIBLE); + mDivider.setTransitionAlpha(1); + mAddIcon.setVisibility(View.VISIBLE); + mAddIcon.setTransitionAlpha(1); + mAddIcon.setOnClickListener(v -> onEndItemClick()); + } else { + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + } mTitleIcon.setImageDrawable(getSpeakerDrawable()); final CharSequence sessionName = mController.getSessionName(); final CharSequence title = TextUtils.isEmpty(sessionName) @@ -206,6 +217,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return; } + mCurrentActivePosition = -1; playSwitchingAnim(mConnectedItem, view); mController.connectDevice(device); device.setState(MediaDeviceState.STATE_CONNECTING); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index bcef43c93be4..0890841eb4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -64,10 +64,12 @@ public abstract class MediaOutputBaseAdapter extends Context mContext; View mHolderView; boolean mIsDragging; + int mCurrentActivePosition; public MediaOutputBaseAdapter(MediaOutputController controller) { mController = controller; mIsDragging = false; + mCurrentActivePosition = -1; } @Override @@ -99,6 +101,10 @@ public abstract class MediaOutputBaseAdapter extends return mIsAnimating; } + int getCurrentActivePosition() { + return mCurrentActivePosition; + } + /** * ViewHolder for binding device view. */ @@ -136,7 +142,7 @@ public abstract class MediaOutputBaseAdapter extends mCheckBox = view.requireViewById(R.id.check_box); } - void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) { mDeviceId = device.getId(); ThreadUtils.postOnBackgroundThread(() -> { Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext); @@ -214,6 +220,9 @@ public abstract class MediaOutputBaseAdapter extends } void initSeekbar(MediaDevice device) { + if (!mController.isVolumeControlEnabled(device)) { + disableSeekBar(); + } mSeekBar.setMax(device.getMaxVolume()); mSeekBar.setMin(0); final int currentVolume = device.getCurrentVolume(); @@ -242,6 +251,7 @@ public abstract class MediaOutputBaseAdapter extends } void initSessionSeekbar() { + disableSeekBar(); mSeekBar.setMax(mController.getSessionVolumeMax()); mSeekBar.setMin(0); final int currentVolume = mController.getSessionVolume(); @@ -330,5 +340,10 @@ public abstract class MediaOutputBaseAdapter extends PorterDuff.Mode.SRC_IN)); return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); } + + private void disableSeekBar() { + mSeekBar.setEnabled(false); + mSeekBar.setOnTouchListener((v, event) -> true); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 8a9a6e694658..cdcdf9a1d4de 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -174,7 +174,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mHeaderTitle.setGravity(Gravity.NO_GRAVITY); } if (!mAdapter.isDragging() && !mAdapter.isAnimating()) { - mAdapter.notifyDataSetChanged(); + int currentActivePosition = mAdapter.getCurrentActivePosition(); + if (currentActivePosition >= 0) { + mAdapter.notifyItemChanged(currentActivePosition); + } else { + mAdapter.notifyDataSetChanged(); + } } // Show when remote media session is available mStopButton.setVisibility(getStopButtonVisibility()); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 8fee9253a421..5293c8850267 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -465,6 +465,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK)); } + boolean isVolumeControlEnabled(@NonNull MediaDevice device) { + return !device.getFeatures().contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK); + } + private final MediaController.Callback mCb = new MediaController.Callback() { @Override public void onMetadataChanged(MediaMetadata metadata) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java index 24e076bb22f1..968c3506f39f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java @@ -68,7 +68,7 @@ public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter { final int size = mGroupMediaDevices.size(); if (newPosition < size) { viewHolder.onBind(mGroupMediaDevices.get(newPosition), false /* topMargin */, - newPosition == (size - 1) /* bottomMargin */); + newPosition == (size - 1) /* bottomMargin */, position); return; } if (DEBUG) { @@ -94,8 +94,8 @@ public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter { } @Override - void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { - super.onBind(device, topMargin, bottomMargin); + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) { + super.onBind(device, topMargin, bottomMargin, position); mDivider.setVisibility(View.GONE); mAddIcon.setVisibility(View.GONE); mBottomDivider.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index c6d7e7c46abb..26f38ddd5919 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -136,6 +136,7 @@ import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.AutoHideController; @@ -201,6 +202,7 @@ public class NavigationBar implements View.OnAttachStateChangeListener, private final NavigationBarOverlayController mNavbarOverlayController; private final UiEventLogger mUiEventLogger; private final UserTracker mUserTracker; + private final NotificationShadeDepthController mNotificationShadeDepthController; private Bundle mSavedState; private NavigationBarView mNavigationBarView; @@ -234,7 +236,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, private boolean mTransientShown; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; - private int mA11yBtnMode; private LightBarController mLightBarController; private AutoHideController mAutoHideController; @@ -439,6 +440,25 @@ public class NavigationBar implements View.OnAttachStateChangeListener, } }; + private final NotificationShadeDepthController.DepthListener mDepthListener = + new NotificationShadeDepthController.DepthListener() { + boolean mHasBlurs; + + @Override + public void onWallpaperZoomOutChanged(float zoomOut) { + } + + @Override + public void onBlurRadiusChanged(int radius) { + boolean hasBlurs = radius != 0; + if (hasBlurs == mHasBlurs) { + return; + } + mHasBlurs = hasBlurs; + mNavigationBarView.setWindowHasBlurs(hasBlurs); + } + }; + public NavigationBar(Context context, WindowManager windowManager, Lazy<AssistManager> assistManagerLazy, @@ -458,6 +478,7 @@ public class NavigationBar implements View.OnAttachStateChangeListener, Optional<Recents> recentsOptional, Lazy<StatusBar> statusBarLazy, ShadeController shadeController, NotificationRemoteInputManager notificationRemoteInputManager, + NotificationShadeDepthController notificationShadeDepthController, SystemActions systemActions, @Main Handler mainHandler, NavigationBarOverlayController navbarOverlayController, @@ -488,10 +509,10 @@ public class NavigationBar implements View.OnAttachStateChangeListener, mNavbarOverlayController = navbarOverlayController; mUiEventLogger = uiEventLogger; mUserTracker = userTracker; + mNotificationShadeDepthController = notificationShadeDepthController; mNavBarMode = mNavigationModeController.addListener(this); mAccessibilityButtonModeObserver.addListener(this); - mA11yBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); } public NavigationBarView getView() { @@ -572,6 +593,7 @@ public class NavigationBar implements View.OnAttachStateChangeListener, mIsCurrentUserSetup = mDeviceProvisionedController.isCurrentUserSetup(); mDeviceProvisionedController.addCallback(mUserSetupListener); + mNotificationShadeDepthController.addListener(mDepthListener); setAccessibilityFloatingMenuModeIfNeeded(); @@ -588,6 +610,7 @@ public class NavigationBar implements View.OnAttachStateChangeListener, mAccessibilityManagerWrapper.removeCallback(mAccessibilityListener); mContentResolver.unregisterContentObserver(mAssistContentObserver); mDeviceProvisionedController.removeCallback(mUserSetupListener); + mNotificationShadeDepthController.removeListener(mDepthListener); DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); } @@ -1379,8 +1402,9 @@ public class NavigationBar implements View.OnAttachStateChangeListener, private void setAccessibilityFloatingMenuModeIfNeeded() { if (QuickStepContract.isGesturalMode(mNavBarMode)) { - Settings.Secure.putInt(mContentResolver, Settings.Secure.ACCESSIBILITY_BUTTON_MODE, - ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); + Settings.Secure.putIntForUser(mContentResolver, + Settings.Secure.ACCESSIBILITY_BUTTON_MODE, + ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, UserHandle.USER_CURRENT); } } @@ -1441,7 +1465,8 @@ public class NavigationBar implements View.OnAttachStateChangeListener, // If accessibility button is floating menu mode, click and long click state should be // disabled. - if (mA11yBtnMode == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) { + if (mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode() + == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) { return 0; } @@ -1552,7 +1577,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, @Override public void onAccessibilityButtonModeChanged(int mode) { - mA11yBtnMode = mode; updateAccessibilityServicesState(mAccessibilityManager); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 53592101c3ea..b9e9240b354a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -61,6 +61,7 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBar; @@ -115,6 +116,7 @@ public class NavigationBarController implements Callbacks, private final DisplayManager mDisplayManager; private final NavigationBarOverlayController mNavBarOverlayController; private final TaskbarDelegate mTaskbarDelegate; + private final NotificationShadeDepthController mNotificationShadeDepthController; private int mNavMode; private boolean mIsTablet; private final UserTracker mUserTracker; @@ -149,6 +151,7 @@ public class NavigationBarController implements Callbacks, Lazy<StatusBar> statusBarLazy, ShadeController shadeController, NotificationRemoteInputManager notificationRemoteInputManager, + NotificationShadeDepthController notificationShadeDepthController, SystemActions systemActions, @Main Handler mainHandler, UiEventLogger uiEventLogger, @@ -175,6 +178,7 @@ public class NavigationBarController implements Callbacks, mStatusBarLazy = statusBarLazy; mShadeController = shadeController; mNotificationRemoteInputManager = notificationRemoteInputManager; + mNotificationShadeDepthController = notificationShadeDepthController; mSystemActions = systemActions; mUiEventLogger = uiEventLogger; mHandler = mainHandler; @@ -362,6 +366,7 @@ public class NavigationBarController implements Callbacks, mStatusBarLazy, mShadeController, mNotificationRemoteInputManager, + mNotificationShadeDepthController, mSystemActions, mHandler, mNavBarOverlayController, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 7af4853dd3f2..4816f1cf8d6a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -413,6 +413,13 @@ public class NavigationBarView extends FrameLayout implements return super.onTouchEvent(event); } + /** + * If we're blurring the shade window. + */ + public void setWindowHasBlurs(boolean hasBlurs) { + mRegionSamplingHelper.setWindowHasBlurs(hasBlurs); + } + void onTransientStateChanged(boolean isTransient) { mEdgeBackGestureHandler.onNavBarTransientStateChanged(isTransient); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java index 70117eb6d2f0..560d89af8e92 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java @@ -65,6 +65,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, private final float mLuminanceChangeThreshold; private boolean mFirstSamplingAfterStart; private boolean mWindowVisible; + private boolean mWindowHasBlurs; private SurfaceControl mRegisteredStopLayer = null; private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() { @Override @@ -153,6 +154,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() && mWindowVisible + && !mWindowHasBlurs && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart); if (isSamplingEnabled) { ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl(); @@ -225,6 +227,14 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, updateSamplingListener(); } + /** + * If we're blurring the shade window. + */ + public void setWindowHasBlurs(boolean hasBlurs) { + mWindowHasBlurs = hasBlurs; + updateSamplingListener(); + } + public void dump(PrintWriter pw) { pw.println("RegionSamplingHelper:"); pw.println(" sampleView isAttached: " + mSampledView.isAttachedToWindow()); @@ -238,6 +248,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, pw.println(" mLastMedianLuma: " + mLastMedianLuma); pw.println(" mCurrentMedianLuma: " + mCurrentMedianLuma); pw.println(" mWindowVisible: " + mWindowVisible); + pw.println(" mWindowHasBlurs: " + mWindowHasBlurs); pw.println(" mWaitingOnDraw: " + mWaitingOnDraw); pw.println(" mRegisteredStopLayer: " + mRegisteredStopLayer); pw.println(" mIsDestroyed: " + mIsDestroyed); diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java b/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java new file mode 100644 index 000000000000..3bc1f30ea321 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2021 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.people; + +import static com.android.systemui.people.PeopleSpaceUtils.DEBUG; +import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; +import static com.android.systemui.people.PeopleSpaceUtils.removeSharedPreferencesStorageForTile; +import static com.android.systemui.people.widget.PeopleBackupHelper.isReadyForRestore; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.people.IPeopleManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.PersistableBundle; +import android.os.ServiceManager; +import android.preference.PreferenceManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.people.widget.PeopleBackupHelper; +import com.android.systemui.people.widget.PeopleTileKey; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Follow-up job that runs after a Conversations widgets restore operation. Check if shortcuts that + * were not available before are now available. If any shortcut doesn't become available after + * 1 day, we clean up its storage. + */ +public class PeopleBackupFollowUpJob extends JobService { + private static final String TAG = "PeopleBackupFollowUpJob"; + private static final String START_DATE = "start_date"; + + /** Follow-up job id. */ + public static final int JOB_ID = 74823873; + + private static final long JOB_PERIODIC_DURATION = Duration.ofHours(6).toMillis(); + private static final long CLEAN_UP_STORAGE_AFTER_DURATION = Duration.ofHours(48).toMillis(); + + /** SharedPreferences file name for follow-up specific storage.*/ + public static final String SHARED_FOLLOW_UP = "shared_follow_up"; + + private final Object mLock = new Object(); + private Context mContext; + private PackageManager mPackageManager; + private IPeopleManager mIPeopleManager; + private JobScheduler mJobScheduler; + + /** Schedules a PeopleBackupFollowUpJob every 2 hours. */ + public static void scheduleJob(Context context) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + PersistableBundle bundle = new PersistableBundle(); + bundle.putLong(START_DATE, System.currentTimeMillis()); + JobInfo jobInfo = new JobInfo + .Builder(JOB_ID, new ComponentName(context, PeopleBackupFollowUpJob.class)) + .setPeriodic(JOB_PERIODIC_DURATION) + .setExtras(bundle) + .build(); + jobScheduler.schedule(jobInfo); + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = getApplicationContext(); + mPackageManager = getApplicationContext().getPackageManager(); + mIPeopleManager = IPeopleManager.Stub.asInterface( + ServiceManager.getService(Context.PEOPLE_SERVICE)); + mJobScheduler = mContext.getSystemService(JobScheduler.class); + + } + + /** Sets necessary managers for testing. */ + @VisibleForTesting + public void setManagers(Context context, PackageManager packageManager, + IPeopleManager iPeopleManager, JobScheduler jobScheduler) { + mContext = context; + mPackageManager = packageManager; + mIPeopleManager = iPeopleManager; + mJobScheduler = jobScheduler; + } + + @Override + public boolean onStartJob(JobParameters params) { + if (DEBUG) Log.d(TAG, "Starting job."); + synchronized (mLock) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sp.edit(); + SharedPreferences followUp = this.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + + // Remove from SHARED_FOLLOW_UP storage all widgets that are now ready to be updated. + Map<String, Set<String>> remainingWidgets = + processFollowUpFile(followUp, followUpEditor); + + // Check if all widgets were restored or if enough time elapsed to cancel the job. + long start = params.getExtras().getLong(START_DATE); + long now = System.currentTimeMillis(); + if (shouldCancelJob(remainingWidgets, start, now)) { + cancelJobAndClearRemainingWidgets(remainingWidgets, followUpEditor, sp); + } + + editor.apply(); + followUpEditor.apply(); + } + + // Ensure all widgets modified from SHARED_FOLLOW_UP storage are now updated. + PeopleBackupHelper.updateWidgets(mContext); + return false; + } + + /** + * Iterates through follow-up file entries and checks which shortcuts are now available. + * Returns a map of shortcuts that should be checked at a later time. + */ + public Map<String, Set<String>> processFollowUpFile(SharedPreferences followUp, + SharedPreferences.Editor followUpEditor) { + Map<String, Set<String>> remainingWidgets = new HashMap<>(); + Map<String, ?> all = followUp.getAll(); + for (Map.Entry<String, ?> entry : all.entrySet()) { + String key = entry.getKey(); + + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); + boolean restored = isReadyForRestore(mIPeopleManager, mPackageManager, peopleTileKey); + if (restored) { + if (DEBUG) Log.d(TAG, "Removing key from follow-up: " + key); + followUpEditor.remove(key); + continue; + } + + if (DEBUG) Log.d(TAG, "Key should not be restored yet, try later: " + key); + try { + remainingWidgets.put(entry.getKey(), (Set<String>) entry.getValue()); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + } + } + return remainingWidgets; + } + + /** Returns whether all shortcuts were restored or if enough time elapsed to cancel the job. */ + public boolean shouldCancelJob(Map<String, Set<String>> remainingWidgets, + long start, long now) { + if (remainingWidgets.isEmpty()) { + if (DEBUG) Log.d(TAG, "All widget storage was successfully restored."); + return true; + } + + boolean oneDayHasPassed = (now - start) > CLEAN_UP_STORAGE_AFTER_DURATION; + if (oneDayHasPassed) { + if (DEBUG) { + Log.w(TAG, "One or more widgets were not properly restored, " + + "but cancelling job because it has been a day."); + } + return true; + } + if (DEBUG) Log.d(TAG, "There are still non-restored widgets, run job again."); + return false; + } + + /** Cancels job and removes storage of any shortcut that was not restored. */ + public void cancelJobAndClearRemainingWidgets(Map<String, Set<String>> remainingWidgets, + SharedPreferences.Editor followUpEditor, SharedPreferences sp) { + if (DEBUG) Log.d(TAG, "Cancelling follow up job."); + removeUnavailableShortcutsFromSharedStorage(remainingWidgets, sp); + followUpEditor.clear(); + mJobScheduler.cancel(JOB_ID); + } + + private void removeUnavailableShortcutsFromSharedStorage(Map<String, + Set<String>> remainingWidgets, SharedPreferences sp) { + for (Map.Entry<String, Set<String>> entry : remainingWidgets.entrySet()) { + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(entry.getKey()); + if (!PeopleTileKey.isValid(peopleTileKey)) { + Log.e(TAG, "Malformed peopleTileKey in follow-up file: " + entry.getKey()); + continue; + } + Set<String> widgetIds; + try { + widgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed widget ids in follow-up file: " + e); + continue; + } + for (String id : widgetIds) { + int widgetId; + try { + widgetId = Integer.parseInt(id); + } catch (NumberFormatException ex) { + Log.e(TAG, "Malformed widget id in follow-up file: " + ex); + continue; + } + + String contactUriString = sp.getString(String.valueOf(widgetId), EMPTY_STRING); + removeSharedPreferencesStorageForTile( + mContext, peopleTileKey, widgetId, contactUriString); + } + } + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index d9e2648750a4..93a3f81fdd6b 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -89,7 +89,7 @@ public class PeopleSpaceActivity extends Activity { // The Tile preview has colorBackground as its background. Change it so it's different // than the activity's background. - LinearLayout item = findViewById(R.id.item); + LinearLayout item = findViewById(android.R.id.background); GradientDrawable shape = (GradientDrawable) item.getBackground(); final TypedArray ta = mContext.getTheme().obtainStyledAttributes( new int[]{com.android.internal.R.attr.colorSurface}); diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java index 917a060f1f1d..c01d6dcd7d64 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java @@ -25,6 +25,7 @@ import static com.android.systemui.people.NotificationHelper.shouldMatchNotifica import android.annotation.Nullable; import android.app.Notification; +import android.app.backup.BackupManager; import android.app.people.ConversationChannel; import android.app.people.IPeopleManager; import android.app.people.PeopleSpaceTile; @@ -74,7 +75,8 @@ import java.util.stream.Stream; /** Utils class for People Space. */ public class PeopleSpaceUtils { /** Turns on debugging information about People Space. */ - public static final boolean DEBUG = true; + public static final boolean DEBUG = false; + public static final String PACKAGE_NAME = "package_name"; public static final String USER_ID = "user_id"; public static final String SHORTCUT_ID = "shortcut_id"; @@ -89,7 +91,7 @@ public class PeopleSpaceUtils { /** Returns stored widgets for the conversation specified. */ public static Set<String> getStoredWidgetIds(SharedPreferences sp, PeopleTileKey key) { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { return new HashSet<>(); } return new HashSet<>(sp.getStringSet(key.toString(), new HashSet<>())); @@ -97,19 +99,16 @@ public class PeopleSpaceUtils { /** Sets all relevant storage for {@code appWidgetId} association to {@code tile}. */ public static void setSharedPreferencesStorageForTile(Context context, PeopleTileKey key, - int appWidgetId, Uri contactUri) { - if (!key.isValid()) { + int appWidgetId, Uri contactUri, BackupManager backupManager) { + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Not storing for invalid key"); return; } // Write relevant persisted storage. SharedPreferences widgetSp = context.getSharedPreferences(String.valueOf(appWidgetId), Context.MODE_PRIVATE); - SharedPreferences.Editor widgetEditor = widgetSp.edit(); - widgetEditor.putString(PeopleSpaceUtils.PACKAGE_NAME, key.getPackageName()); - widgetEditor.putString(PeopleSpaceUtils.SHORTCUT_ID, key.getShortcutId()); - widgetEditor.putInt(PeopleSpaceUtils.USER_ID, key.getUserId()); - widgetEditor.apply(); + SharedPreferencesHelper.setPeopleTileKey(widgetSp, key); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sp.edit(); String contactUriString = contactUri == null ? EMPTY_STRING : contactUri.toString(); @@ -117,14 +116,18 @@ public class PeopleSpaceUtils { // Don't overwrite existing widgets with the same key. addAppWidgetIdForKey(sp, editor, appWidgetId, key.toString()); - addAppWidgetIdForKey(sp, editor, appWidgetId, contactUriString); + if (!TextUtils.isEmpty(contactUriString)) { + addAppWidgetIdForKey(sp, editor, appWidgetId, contactUriString); + } editor.apply(); + backupManager.dataChanged(); } /** Removes stored data when tile is deleted. */ public static void removeSharedPreferencesStorageForTile(Context context, PeopleTileKey key, int widgetId, String contactUriString) { // Delete widgetId mapping to key. + if (DEBUG) Log.d(TAG, "Removing widget info from sharedPrefs"); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sp.edit(); editor.remove(String.valueOf(widgetId)); @@ -230,7 +233,7 @@ public class PeopleSpaceUtils { */ public static PeopleSpaceTile augmentTileFromNotification(Context context, PeopleSpaceTile tile, PeopleTileKey key, NotificationEntry notificationEntry, int messagesCount, - Optional<Integer> appWidgetId) { + Optional<Integer> appWidgetId, BackupManager backupManager) { if (notificationEntry == null || notificationEntry.getSbn().getNotification() == null) { if (DEBUG) Log.d(TAG, "Tile key: " + key.toString() + ". Notification is null"); return removeNotificationFields(tile); @@ -246,7 +249,7 @@ public class PeopleSpaceUtils { Uri contactUri = Uri.parse(uriFromNotification); // Update storage. setSharedPreferencesStorageForTile(context, new PeopleTileKey(tile), appWidgetId.get(), - contactUri); + contactUri, backupManager); // Update cached tile in-memory. updatedTile.setContactUri(contactUri); } diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java index 96aeb60ae93c..4ee951f3cdb1 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java @@ -211,7 +211,8 @@ class PeopleStoryIconFactory implements AutoCloseable { @Override public void setColorFilter(ColorFilter colorFilter) { - // unimplemented + if (mAvatar != null) mAvatar.setColorFilter(colorFilter); + if (mBadgeIcon != null) mBadgeIcon.setColorFilter(colorFilter); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java index 0e17c8bfcab5..7ab4b6f200ed 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java @@ -29,6 +29,9 @@ import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; +import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_SIZES; +import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.util.TypedValue.COMPLEX_UNIT_PX; import static com.android.systemui.people.PeopleSpaceUtils.STARRED_CONTACT; import static com.android.systemui.people.PeopleSpaceUtils.VALID_CONTACT; @@ -41,25 +44,33 @@ import android.app.people.ConversationStatus; import android.app.people.PeopleSpaceTile; import android.content.Context; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.graphics.text.LineBreaker; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; +import android.text.StaticLayout; +import android.text.TextPaint; import android.text.TextUtils; import android.util.IconDrawableFactory; import android.util.Log; import android.util.Pair; +import android.util.SizeF; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.widget.RemoteViews; import android.widget.TextView; +import androidx.annotation.DimenRes; +import androidx.annotation.Px; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import androidx.core.math.MathUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.launcher3.icons.FastBitmapDrawable; @@ -75,8 +86,10 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -84,7 +97,7 @@ import java.util.stream.Collectors; /** Functions that help creating the People tile layouts. */ public class PeopleTileViewHelper { /** Turns on debugging information about People Space. */ - public static final boolean DEBUG = true; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; private static final String TAG = "PeopleTileView"; private static final int DAYS_IN_A_WEEK = 7; @@ -103,16 +116,22 @@ public class PeopleTileViewHelper { private static final int MIN_MEDIUM_VERTICAL_PADDING = 4; private static final int MAX_MEDIUM_PADDING = 16; private static final int FIXED_HEIGHT_DIMENS_FOR_MEDIUM_CONTENT_BEFORE_PADDING = 8 + 4; - private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL = 6 + 4 + 8; - private static final int FIXED_WIDTH_DIMENS_FOR_SMALL = 4 + 4; + private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL_VERTICAL = 6 + 4 + 8; + private static final int FIXED_WIDTH_DIMENS_FOR_SMALL_VERTICAL = 4 + 4; + private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL_HORIZONTAL = 6 + 4; + private static final int FIXED_WIDTH_DIMENS_FOR_SMALL_HORIZONTAL = 8 + 8; private static final int MESSAGES_COUNT_OVERFLOW = 6; + private static final CharSequence EMOJI_CAKE = "\ud83c\udf82"; + private static final Pattern DOUBLE_EXCLAMATION_PATTERN = Pattern.compile("[!][!]+"); private static final Pattern DOUBLE_QUESTION_PATTERN = Pattern.compile("[?][?]+"); private static final Pattern ANY_DOUBLE_MARK_PATTERN = Pattern.compile("[!?][!?]+"); private static final Pattern MIXED_MARK_PATTERN = Pattern.compile("![?].*|.*[?]!"); + static final String BRIEF_PAUSE_ON_TALKBACK = "\n\n"; + // This regex can be used to match Unicode emoji characters and character sequences. It's from // the official Unicode site (https://unicode.org/reports/tr51/#EBNF_and_Regex) with minor // changes to fit our needs. It should be updated once new emoji categories are added. @@ -163,28 +182,64 @@ public class PeopleTileViewHelper { private Locale mLocale; private NumberFormat mIntegerFormat; - public PeopleTileViewHelper(Context context, @Nullable PeopleSpaceTile tile, - int appWidgetId, Bundle options, PeopleTileKey key) { + PeopleTileViewHelper(Context context, @Nullable PeopleSpaceTile tile, + int appWidgetId, int width, int height, PeopleTileKey key) { mContext = context; mTile = tile; mKey = key; mAppWidgetId = appWidgetId; mDensity = mContext.getResources().getDisplayMetrics().density; - int display = mContext.getResources().getConfiguration().orientation; - mWidth = display == Configuration.ORIENTATION_PORTRAIT - ? options.getInt(OPTION_APPWIDGET_MIN_WIDTH, - getSizeInDp(R.dimen.default_width)) : options.getInt( - OPTION_APPWIDGET_MAX_WIDTH, - getSizeInDp(R.dimen.default_width)); - mHeight = display == Configuration.ORIENTATION_PORTRAIT ? options.getInt( - OPTION_APPWIDGET_MAX_HEIGHT, - getSizeInDp(R.dimen.default_height)) - : options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, - getSizeInDp(R.dimen.default_height)); + mWidth = width; + mHeight = height; mLayoutSize = getLayoutSize(); } - public RemoteViews getViews() { + /** + * Creates a {@link RemoteViews} for the specified arguments. The RemoteViews will support all + * the sizes present in {@code options.}. + */ + public static RemoteViews createRemoteViews(Context context, @Nullable PeopleSpaceTile tile, + int appWidgetId, Bundle options, PeopleTileKey key) { + List<SizeF> widgetSizes = getWidgetSizes(context, options); + Map<SizeF, RemoteViews> sizeToRemoteView = + widgetSizes + .stream() + .distinct() + .collect(Collectors.toMap( + Function.identity(), + size -> new PeopleTileViewHelper( + context, tile, appWidgetId, + (int) size.getWidth(), + (int) size.getHeight(), + key) + .getViews())); + return new RemoteViews(sizeToRemoteView); + } + + private static List<SizeF> getWidgetSizes(Context context, Bundle options) { + float density = context.getResources().getDisplayMetrics().density; + List<SizeF> widgetSizes = options.getParcelableArrayList(OPTION_APPWIDGET_SIZES); + // If the full list of sizes was provided in the options bundle, use that. + if (widgetSizes != null && !widgetSizes.isEmpty()) return widgetSizes; + + // Otherwise, create a list using the portrait/landscape sizes. + int defaultWidth = getSizeInDp(context, R.dimen.default_width, density); + int defaultHeight = getSizeInDp(context, R.dimen.default_height, density); + widgetSizes = new ArrayList<>(2); + + int portraitWidth = options.getInt(OPTION_APPWIDGET_MIN_WIDTH, defaultWidth); + int portraitHeight = options.getInt(OPTION_APPWIDGET_MAX_HEIGHT, defaultHeight); + widgetSizes.add(new SizeF(portraitWidth, portraitHeight)); + + int landscapeWidth = options.getInt(OPTION_APPWIDGET_MAX_WIDTH, defaultWidth); + int landscapeHeight = options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, defaultHeight); + widgetSizes.add(new SizeF(landscapeWidth, landscapeHeight)); + + return widgetSizes; + } + + @VisibleForTesting + RemoteViews getViews() { RemoteViews viewsForTile = getViewForTile(); int maxAvatarSize = getMaxAvatarSize(viewsForTile); RemoteViews views = setCommonRemoteViewsFields(viewsForTile, maxAvatarSize); @@ -197,12 +252,16 @@ public class PeopleTileViewHelper { */ private RemoteViews getViewForTile() { if (DEBUG) Log.d(TAG, "Creating view for tile key: " + mKey.toString()); - if (mTile == null || mTile.isPackageSuspended() || mTile.isUserQuieted() - || isDndBlockingTileData(mTile)) { + if (mTile == null || mTile.isPackageSuspended() || mTile.isUserQuieted()) { if (DEBUG) Log.d(TAG, "Create suppressed view: " + mTile); return createSuppressedView(); } + if (isDndBlockingTileData(mTile)) { + if (DEBUG) Log.d(TAG, "Create dnd view"); + return createDndRemoteViews().mRemoteViews; + } + if (Objects.equals(mTile.getNotificationCategory(), CATEGORY_MISSED_CALL)) { if (DEBUG) Log.d(TAG, "Create missed call view"); return createMissedCallRemoteViews(); @@ -236,7 +295,9 @@ public class PeopleTileViewHelper { return createLastInteractionRemoteViews(); } - private boolean isDndBlockingTileData(PeopleSpaceTile tile) { + private static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) { + if (tile == null) return false; + int notificationPolicyState = tile.getNotificationPolicyState(); if ((notificationPolicyState & PeopleSpaceTile.SHOW_CONVERSATIONS) != 0) { // Not in DND, or all conversations @@ -273,11 +334,8 @@ public class PeopleTileViewHelper { R.layout.people_tile_suppressed_layout); } Drawable appIcon = mContext.getDrawable(R.drawable.ic_conversation_icon); - Bitmap appIconAsBitmap = convertDrawableToBitmap(appIcon); - FastBitmapDrawable drawable = new FastBitmapDrawable(appIconAsBitmap); - drawable.setIsDisabled(true); - Bitmap convertedBitmap = convertDrawableToBitmap(drawable); - views.setImageViewBitmap(R.id.icon, convertedBitmap); + Bitmap disabledBitmap = convertDrawableToDisabledBitmap(appIcon); + views.setImageViewBitmap(R.id.icon, disabledBitmap); return views; } @@ -350,7 +408,8 @@ public class PeopleTileViewHelper { return LAYOUT_LARGE; } // Small layout used below a certain minimum mWidth with any mHeight. - if (mWidth >= getSizeInDp(R.dimen.required_width_for_medium)) { + if (mHeight >= getSizeInDp(R.dimen.required_height_for_medium) + && mWidth >= getSizeInDp(R.dimen.required_width_for_medium)) { int spaceAvailableForPadding = mHeight - (getSizeInDp(R.dimen.avatar_size_for_medium) + 4 + getLineHeightFromResource( @@ -383,10 +442,15 @@ public class PeopleTileViewHelper { // Calculate adaptive avatar size for remaining layouts. if (layoutId == R.layout.people_tile_small) { - int avatarHeightSpace = mHeight - (FIXED_HEIGHT_DIMENS_FOR_SMALL + Math.max(18, + int avatarHeightSpace = mHeight - (FIXED_HEIGHT_DIMENS_FOR_SMALL_VERTICAL + Math.max(18, getLineHeightFromResource( R.dimen.name_text_size_for_small))); - int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL; + int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL_VERTICAL; + avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace); + } + if (layoutId == R.layout.people_tile_small_horizontal) { + int avatarHeightSpace = mHeight - FIXED_HEIGHT_DIMENS_FOR_SMALL_HORIZONTAL; + int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL_HORIZONTAL; avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace); } @@ -413,6 +477,11 @@ public class PeopleTileViewHelper { int avatarWidthSpace = mWidth - (14 + 14); avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace); } + + if (isDndBlockingTileData(mTile) && mLayoutSize != LAYOUT_SMALL) { + avatarSize = createDndRemoteViews().mAvatarSize; + } + return Math.min(avatarSize, getSizeInDp(R.dimen.max_people_avatar_size)); } @@ -426,15 +495,33 @@ public class PeopleTileViewHelper { boolean isAvailable = mTile.getStatuses() != null && mTile.getStatuses().stream().anyMatch( c -> c.getAvailability() == AVAILABILITY_AVAILABLE); + + int startPadding; if (isAvailable) { views.setViewVisibility(R.id.availability, View.VISIBLE); + startPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.availability_dot_shown_padding); } else { views.setViewVisibility(R.id.availability, View.GONE); + startPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.availability_dot_missing_padding); } + boolean isLeftToRight = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_LTR; + views.setViewPadding(R.id.padding_before_availability, + isLeftToRight ? startPadding : 0, 0, isLeftToRight ? 0 : startPadding, + 0); - views.setBoolean(R.id.image, "setClipToOutline", true); + boolean hasNewStory = getHasNewStory(mTile); views.setImageViewBitmap(R.id.person_icon, - getPersonIconBitmap(mContext, mTile, maxAvatarSize)); + getPersonIconBitmap(mContext, mTile, maxAvatarSize, hasNewStory)); + if (hasNewStory) { + views.setContentDescription(R.id.person_icon, + mContext.getString(R.string.new_story_status_content_description, + mTile.getUserName())); + } else { + views.setContentDescription(R.id.person_icon, null); + } return views; } catch (Exception e) { Log.e(TAG, "Failed to set common fields: " + e); @@ -442,7 +529,17 @@ public class PeopleTileViewHelper { return views; } + private static boolean getHasNewStory(PeopleSpaceTile tile) { + return tile.getStatuses() != null && tile.getStatuses().stream().anyMatch( + c -> c.getActivity() == ACTIVITY_NEW_STORY); + } + private RemoteViews setLaunchIntents(RemoteViews views) { + if (!PeopleTileKey.isValid(mKey) || mTile == null) { + if (DEBUG) Log.d(TAG, "Skipping launch intent, Null tile or invalid key: " + mKey); + return views; + } + try { Intent activityIntent = new Intent(mContext, LaunchConversationActivity.class); activityIntent.addFlags( @@ -460,7 +557,7 @@ public class PeopleTileViewHelper { PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY, mTile.getNotificationKey()); } - views.setOnClickPendingIntent(R.id.item, PendingIntent.getActivity( + views.setOnClickPendingIntent(android.R.id.background, PendingIntent.getActivity( mContext, mAppWidgetId, activityIntent, @@ -473,6 +570,87 @@ public class PeopleTileViewHelper { return views; } + private RemoteViewsAndSizes createDndRemoteViews() { + RemoteViews views = new RemoteViews(mContext.getPackageName(), getViewForDndRemoteViews()); + + int mediumAvatarSize = getSizeInDp(R.dimen.avatar_size_for_medium_empty); + int maxAvatarSize = getSizeInDp(R.dimen.max_people_avatar_size); + + String text = mContext.getString(R.string.paused_by_dnd); + views.setTextViewText(R.id.text_content, text); + + int textSizeResId = + mLayoutSize == LAYOUT_LARGE + ? R.dimen.content_text_size_for_large + : R.dimen.content_text_size_for_medium; + float textSizePx = mContext.getResources().getDimension(textSizeResId); + views.setTextViewTextSize(R.id.text_content, COMPLEX_UNIT_PX, textSizePx); + int lineHeight = getLineHeightFromResource(textSizeResId); + + int avatarSize; + if (mLayoutSize == LAYOUT_MEDIUM) { + int maxTextHeight = mHeight - 16; + views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight); + avatarSize = mediumAvatarSize; + } else { + int outerPadding = 16; + int outerPaddingTop = outerPadding - 2; + int outerPaddingPx = dpToPx(outerPadding); + int outerPaddingTopPx = dpToPx(outerPaddingTop); + int iconSize = + getSizeInDp( + mLayoutSize == LAYOUT_SMALL + ? R.dimen.regular_predefined_icon + : R.dimen.largest_predefined_icon); + int heightWithoutIcon = mHeight - 2 * outerPadding - iconSize; + int paddingBetweenElements = + getSizeInDp(R.dimen.padding_between_suppressed_layout_items); + int maxTextWidth = mWidth - outerPadding * 2; + int maxTextHeight = heightWithoutIcon - mediumAvatarSize - paddingBetweenElements * 2; + + int availableAvatarHeight; + int textHeight = estimateTextHeight(text, textSizeResId, maxTextWidth); + if (textHeight <= maxTextHeight && mLayoutSize == LAYOUT_LARGE) { + // If the text will fit, then display it and deduct its height from the space we + // have for the avatar. + availableAvatarHeight = heightWithoutIcon - textHeight - paddingBetweenElements * 2; + views.setViewVisibility(R.id.text_content, View.VISIBLE); + views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight); + views.setContentDescription(R.id.predefined_icon, null); + int availableAvatarWidth = mWidth - outerPadding * 2; + avatarSize = + MathUtils.clamp( + /* value= */ Math.min(availableAvatarWidth, availableAvatarHeight), + /* min= */ dpToPx(10), + /* max= */ maxAvatarSize); + views.setViewPadding( + android.R.id.background, + outerPaddingPx, + outerPaddingTopPx, + outerPaddingPx, + outerPaddingPx); + views.setViewLayoutWidth(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP); + views.setViewLayoutHeight(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP); + } else { + // If expected to use LAYOUT_LARGE, but we found we do not have space for the + // text as calculated above, re-assign the view to the small layout. + if (mLayoutSize != LAYOUT_SMALL) { + views = new RemoteViews(mContext.getPackageName(), R.layout.people_tile_small); + } + avatarSize = getMaxAvatarSize(views); + views.setViewVisibility(R.id.messages_count, View.GONE); + views.setViewVisibility(R.id.name, View.GONE); + // If we don't show the dnd text, set it as the content description on the icon + // for a11y. + views.setContentDescription(R.id.predefined_icon, text); + } + views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); + views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_qs_dnd_on); + } + + return new RemoteViewsAndSizes(views, avatarSize); + } + private RemoteViews createMissedCallRemoteViews() { RemoteViews views = setViewForContentLayout(new RemoteViews(mContext.getPackageName(), getLayoutForContent())); @@ -480,14 +658,16 @@ public class PeopleTileViewHelper { views.setViewVisibility(R.id.text_content, View.VISIBLE); views.setViewVisibility(R.id.messages_count, View.GONE); setMaxLines(views, false); - views.setTextViewText(R.id.text_content, mTile.getNotificationContent()); + CharSequence content = mTile.getNotificationContent(); + views.setTextViewText(R.id.text_content, content); + setContentDescriptionForNotificationTextContent(views, content, mTile.getUserName()); views.setColorAttr(R.id.text_content, "setTextColor", android.R.attr.colorError); views.setColorAttr(R.id.predefined_icon, "setColorFilter", android.R.attr.colorError); views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_phone_missed); if (mLayoutSize == LAYOUT_LARGE) { views.setInt(R.id.content, "setGravity", Gravity.BOTTOM); - views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.large_predefined_icon); - views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.large_predefined_icon); + views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon); + views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon); } setAvailabilityDotPadding(views, R.dimen.availability_dot_notification_padding); return views; @@ -501,12 +681,16 @@ public class PeopleTileViewHelper { if (image != null) { // TODO: Use NotificationInlineImageCache views.setImageViewUri(R.id.image, image); + String newImageDescription = mContext.getString( + R.string.new_notification_image_content_description, mTile.getUserName()); + views.setContentDescription(R.id.image, newImageDescription); views.setViewVisibility(R.id.image, View.VISIBLE); views.setViewVisibility(R.id.text_content, View.GONE); - views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_photo_camera); } else { setMaxLines(views, !TextUtils.isEmpty(sender)); CharSequence content = mTile.getNotificationContent(); + setContentDescriptionForNotificationTextContent(views, content, + sender != null ? sender : mTile.getUserName()); views = decorateBackground(views, content); views.setColorAttr(R.id.text_content, "setTextColor", android.R.attr.textColorPrimary); views.setTextViewText(R.id.text_content, mTile.getNotificationContent()); @@ -536,6 +720,16 @@ public class PeopleTileViewHelper { return views; } + private void setContentDescriptionForNotificationTextContent(RemoteViews views, + CharSequence content, CharSequence sender) { + String newTextDescriptionWithNotificationContent = mContext.getString( + R.string.new_notification_text_content_description, sender, content); + int idForContentDescription = + mLayoutSize == LAYOUT_SMALL ? R.id.predefined_icon : R.id.text_content; + views.setContentDescription(idForContentDescription, + newTextDescriptionWithNotificationContent); + } + // Some messaging apps only include up to 6 messages in their notifications. private String getMessagesCountText(int count) { if (count >= MESSAGES_COUNT_OVERFLOW) { @@ -564,6 +758,11 @@ public class PeopleTileViewHelper { views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); views.setTextViewText(R.id.text_content, statusText); + if (status.getActivity() == ACTIVITY_BIRTHDAY + || status.getActivity() == ACTIVITY_UPCOMING_BIRTHDAY) { + setEmojiBackground(views, EMOJI_CAKE); + } + Icon statusIcon = status.getIcon(); if (statusIcon != null) { // No text content styled text on medium or large. @@ -588,9 +787,56 @@ public class PeopleTileViewHelper { } setAvailabilityDotPadding(views, R.dimen.availability_dot_status_padding); views.setImageViewResource(R.id.predefined_icon, getDrawableForStatus(status)); + CharSequence descriptionForStatus = + getContentDescriptionForStatus(status); + CharSequence customContentDescriptionForStatus = mContext.getString( + R.string.new_status_content_description, mTile.getUserName(), descriptionForStatus); + switch (mLayoutSize) { + case LAYOUT_LARGE: + views.setContentDescription(R.id.text_content, + customContentDescriptionForStatus); + break; + case LAYOUT_MEDIUM: + views.setContentDescription(statusIcon == null ? R.id.text_content : R.id.name, + customContentDescriptionForStatus); + break; + case LAYOUT_SMALL: + views.setContentDescription(R.id.predefined_icon, + customContentDescriptionForStatus); + break; + } return views; } + private CharSequence getContentDescriptionForStatus(ConversationStatus status) { + CharSequence name = mTile.getUserName(); + if (!TextUtils.isEmpty(status.getDescription())) { + return status.getDescription(); + } + switch (status.getActivity()) { + case ACTIVITY_NEW_STORY: + return mContext.getString(R.string.new_story_status_content_description, + name); + case ACTIVITY_ANNIVERSARY: + return mContext.getString(R.string.anniversary_status_content_description, name); + case ACTIVITY_UPCOMING_BIRTHDAY: + return mContext.getString(R.string.upcoming_birthday_status_content_description, + name); + case ACTIVITY_BIRTHDAY: + return mContext.getString(R.string.birthday_status_content_description, name); + case ACTIVITY_LOCATION: + return mContext.getString(R.string.location_status_content_description, name); + case ACTIVITY_GAME: + return mContext.getString(R.string.game_status); + case ACTIVITY_VIDEO: + return mContext.getString(R.string.video_status); + case ACTIVITY_AUDIO: + return mContext.getString(R.string.audio_status); + default: + return EMPTY_STRING; + } + } + private int getDrawableForStatus(ConversationStatus status) { switch (status.getActivity()) { case ACTIVITY_NEW_STORY: @@ -798,6 +1044,11 @@ public class PeopleTileViewHelper { private RemoteViews setViewForContentLayout(RemoteViews views) { views = decorateBackground(views, ""); + views.setContentDescription(R.id.predefined_icon, null); + views.setContentDescription(R.id.text_content, null); + views.setContentDescription(R.id.name, null); + views.setContentDescription(R.id.image, null); + views.setAccessibilityTraversalAfter(R.id.text_content, R.id.name); if (mLayoutSize == LAYOUT_SMALL) { views.setViewVisibility(R.id.predefined_icon, View.VISIBLE); views.setViewVisibility(R.id.name, View.GONE); @@ -884,7 +1135,7 @@ public class PeopleTileViewHelper { return R.layout.people_tile_large_empty; case LAYOUT_SMALL: default: - return R.layout.people_tile_small; + return getLayoutSmallByHeight(); } } @@ -896,7 +1147,7 @@ public class PeopleTileViewHelper { return R.layout.people_tile_large_with_notification_content; case LAYOUT_SMALL: default: - return R.layout.people_tile_small; + return getLayoutSmallByHeight(); } } @@ -908,20 +1159,43 @@ public class PeopleTileViewHelper { return R.layout.people_tile_large_with_status_content; case LAYOUT_SMALL: default: - return R.layout.people_tile_small; + return getLayoutSmallByHeight(); } } + private int getViewForDndRemoteViews() { + switch (mLayoutSize) { + case LAYOUT_MEDIUM: + return R.layout.people_tile_with_suppression_detail_content_horizontal; + case LAYOUT_LARGE: + return R.layout.people_tile_with_suppression_detail_content_vertical; + case LAYOUT_SMALL: + default: + return getLayoutSmallByHeight(); + } + } + + private int getLayoutSmallByHeight() { + if (mHeight >= getSizeInDp(R.dimen.required_height_for_medium)) { + return R.layout.people_tile_small; + } + return R.layout.people_tile_small_horizontal; + } + /** Returns a bitmap with the user icon and package icon. */ - public static Bitmap getPersonIconBitmap( - Context context, PeopleSpaceTile tile, int maxAvatarSize) { - boolean hasNewStory = - tile.getStatuses() != null && tile.getStatuses().stream().anyMatch( - c -> c.getActivity() == ACTIVITY_NEW_STORY); + public static Bitmap getPersonIconBitmap(Context context, PeopleSpaceTile tile, + int maxAvatarSize) { + boolean hasNewStory = getHasNewStory(tile); + return getPersonIconBitmap(context, tile, maxAvatarSize, hasNewStory); + } + /** Returns a bitmap with the user icon and package icon. */ + private static Bitmap getPersonIconBitmap( + Context context, PeopleSpaceTile tile, int maxAvatarSize, boolean hasNewStory) { Icon icon = tile.getUserIcon(); if (icon == null) { - return null; + Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge); + return convertDrawableToDisabledBitmap(placeholder); } PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(context, context.getPackageManager(), @@ -932,6 +1206,14 @@ public class PeopleTileViewHelper { Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable, tile.getPackageName(), getUserId(tile), tile.isImportantConversation(), hasNewStory); + + if (isDndBlockingTileData(tile)) { + // If DND is blocking the conversation, then display the icon in grayscale. + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + personDrawable.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + } + return convertDrawableToBitmap(personDrawable); } @@ -960,4 +1242,76 @@ public class PeopleTileViewHelper { return context.getString(R.string.over_two_weeks_timestamp); } } + + /** + * Estimates the height (in dp) which the text will have given the text size and the available + * width. Returns Integer.MAX_VALUE if the estimation couldn't be obtained, as this is intended + * to be used an estimate of the maximum. + */ + private int estimateTextHeight( + CharSequence text, + @DimenRes int textSizeResId, + int availableWidthDp) { + StaticLayout staticLayout = buildStaticLayout(text, textSizeResId, availableWidthDp); + if (staticLayout == null) { + // Return max value (rather than e.g. -1) so the value can be used with <= bound checks. + return Integer.MAX_VALUE; + } + return pxToDp(staticLayout.getHeight()); + } + + /** + * Builds a StaticLayout for the text given the text size and available width. This can be used + * to obtain information about how TextView will lay out the text. Returns null if any error + * occurred creating a TextView. + */ + @Nullable + private StaticLayout buildStaticLayout( + CharSequence text, + @DimenRes int textSizeResId, + int availableWidthDp) { + try { + TextView textView = new TextView(mContext); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + mContext.getResources().getDimension(textSizeResId)); + textView.setTextAppearance(android.R.style.TextAppearance_DeviceDefault); + TextPaint paint = textView.getPaint(); + return StaticLayout.Builder.obtain( + text, 0, text.length(), paint, dpToPx(availableWidthDp)) + // Simple break strategy avoids hyphenation unless there's a single word longer + // than the line width. We use this break strategy so that we consider text to + // "fit" only if it fits in a nice way (i.e. without hyphenation in the middle + // of words). + .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE) + .build(); + } catch (Exception e) { + Log.e(TAG, "Could not create static layout: " + e); + return null; + } + } + + private int dpToPx(float dp) { + return (int) (dp * mDensity); + } + + private int pxToDp(@Px float px) { + return (int) (px / mDensity); + } + + private static final class RemoteViewsAndSizes { + final RemoteViews mRemoteViews; + final int mAvatarSize; + + RemoteViewsAndSizes(RemoteViews remoteViews, int avatarSize) { + mRemoteViews = remoteViews; + mAvatarSize = avatarSize; + } + } + + private static Bitmap convertDrawableToDisabledBitmap(Drawable icon) { + Bitmap appIconAsBitmap = convertDrawableToBitmap(icon); + FastBitmapDrawable drawable = new FastBitmapDrawable(appIconAsBitmap); + drawable.setIsDisabled(true); + return convertDrawableToBitmap(drawable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java b/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java new file mode 100644 index 000000000000..aef08fb421d8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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.people; + +import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; +import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; +import static com.android.systemui.people.PeopleSpaceUtils.SHORTCUT_ID; +import static com.android.systemui.people.PeopleSpaceUtils.USER_ID; + +import android.content.SharedPreferences; + +import com.android.systemui.people.widget.PeopleTileKey; + +/** Helper class for Conversations widgets SharedPreferences storage. */ +public class SharedPreferencesHelper { + /** Clears all storage from {@code sp}. */ + public static void clear(SharedPreferences sp) { + SharedPreferences.Editor editor = sp.edit(); + editor.clear(); + editor.apply(); + } + + /** Sets {@code sp}'s storage to identify a {@link PeopleTileKey}. */ + public static void setPeopleTileKey(SharedPreferences sp, PeopleTileKey key) { + setPeopleTileKey(sp, key.getShortcutId(), key.getUserId(), key.getPackageName()); + } + + /** Sets {@code sp}'s storage to identify a {@link PeopleTileKey}. */ + public static void setPeopleTileKey(SharedPreferences sp, String shortcutId, int userId, + String packageName) { + SharedPreferences.Editor editor = sp.edit(); + editor.putString(SHORTCUT_ID, shortcutId); + editor.putInt(USER_ID, userId); + editor.putString(PACKAGE_NAME, packageName); + editor.apply(); + } + + /** Returns a {@link PeopleTileKey} based on storage from {@code sp}. */ + public static PeopleTileKey getPeopleTileKey(SharedPreferences sp) { + String shortcutId = sp.getString(SHORTCUT_ID, null); + String packageName = sp.getString(PACKAGE_NAME, null); + int userId = sp.getInt(USER_ID, INVALID_USER_ID); + return new PeopleTileKey(shortcutId, userId, packageName); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java index b031637e4016..79318d69837d 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java @@ -152,7 +152,7 @@ public class LaunchConversationActivity extends Activity { launcherApps.startShortcut( packageName, tileId, null, null, userHandle); } catch (Exception e) { - Log.e(TAG, "Exception:" + e); + Log.e(TAG, "Exception launching shortcut:" + e); } } else { if (DEBUG) Log.d(TAG, "Trying to launch conversation with null shortcutInfo."); diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java new file mode 100644 index 000000000000..d8c96dd182b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2021 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.people.widget; + +import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP; +import static com.android.systemui.people.PeopleSpaceUtils.DEBUG; +import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; +import static com.android.systemui.people.PeopleSpaceUtils.USER_ID; + +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.SharedPreferencesBackupHelper; +import android.app.people.IPeopleManager; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.people.PeopleBackupFollowUpJob; +import com.android.systemui.people.SharedPreferencesHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Helper class to backup and restore Conversations widgets storage. + * It is used by SystemUI's BackupHelper agent. + * TODO(b/192334798): Lock access to storage using PeopleSpaceWidgetManager's lock. + */ +public class PeopleBackupHelper extends SharedPreferencesBackupHelper { + private static final String TAG = "PeopleBackupHelper"; + + public static final String ADD_USER_ID_TO_URI = "add_user_id_to_uri_"; + public static final String SHARED_BACKUP = "shared_backup"; + + private final Context mContext; + private final UserHandle mUserHandle; + private final PackageManager mPackageManager; + private final IPeopleManager mIPeopleManager; + private final AppWidgetManager mAppWidgetManager; + + /** + * Types of entries stored in the default SharedPreferences file for Conversation widgets. + * Widget ID corresponds to a pair [widgetId, contactURI]. + * PeopleTileKey corresponds to a pair [PeopleTileKey, {widgetIds}]. + * Contact URI corresponds to a pair [Contact URI, {widgetIds}]. + */ + enum SharedFileEntryType { + UNKNOWN, + WIDGET_ID, + PEOPLE_TILE_KEY, + CONTACT_URI + } + + /** + * Returns the file names that should be backed up and restored by SharedPreferencesBackupHelper + * infrastructure. + */ + public static List<String> getFilesToBackup() { + return Collections.singletonList(SHARED_BACKUP); + } + + public PeopleBackupHelper(Context context, UserHandle userHandle, + String[] sharedPreferencesKey) { + super(context, sharedPreferencesKey); + mContext = context; + mUserHandle = userHandle; + mPackageManager = context.getPackageManager(); + mIPeopleManager = IPeopleManager.Stub.asInterface( + ServiceManager.getService(Context.PEOPLE_SERVICE)); + mAppWidgetManager = AppWidgetManager.getInstance(context); + } + + @VisibleForTesting + public PeopleBackupHelper(Context context, UserHandle userHandle, + String[] sharedPreferencesKey, PackageManager packageManager, + IPeopleManager peopleManager) { + super(context, sharedPreferencesKey); + mContext = context; + mUserHandle = userHandle; + mPackageManager = packageManager; + mIPeopleManager = peopleManager; + mAppWidgetManager = AppWidgetManager.getInstance(context); + } + + /** + * Reads values from default storage, backs them up appropriately to a specified backup file, + * and calls super's performBackup, which backs up the values of the backup file. + */ + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + if (DEBUG) Log.d(TAG, "Backing up conversation widgets, writing to: " + SHARED_BACKUP); + // Open default value for readings values. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + if (sp.getAll().isEmpty()) { + if (DEBUG) Log.d(TAG, "No information to be backed up, finishing."); + return; + } + + // Open backup file for writing. + SharedPreferences backupSp = mContext.getSharedPreferences( + SHARED_BACKUP, Context.MODE_PRIVATE); + SharedPreferences.Editor backupEditor = backupSp.edit(); + backupEditor.clear(); + + // Fetch Conversations widgets corresponding to this user. + List<String> existingWidgets = getExistingWidgetsForUser(mUserHandle.getIdentifier()); + if (existingWidgets.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing Conversations widgets, returning."); + return; + } + + // Writes each entry to backup file. + sp.getAll().entrySet().forEach(entry -> backupKey(entry, backupEditor, existingWidgets)); + backupEditor.apply(); + + super.performBackup(oldState, data, newState); + } + + /** + * Restores backed up values to backup file via super's restoreEntity, then transfers them + * back to regular storage. Restore operations for each users are done in sequence, so we can + * safely use the same backup file names. + */ + @Override + public void restoreEntity(BackupDataInputStream data) { + if (DEBUG) Log.d(TAG, "Restoring Conversation widgets."); + super.restoreEntity(data); + + // Open backup file for reading values. + SharedPreferences backupSp = mContext.getSharedPreferences( + SHARED_BACKUP, Context.MODE_PRIVATE); + + // Open default file and follow-up file for writing. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + SharedPreferences.Editor editor = sp.edit(); + SharedPreferences followUp = mContext.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + + // Writes each entry back to default value. + boolean shouldScheduleJob = false; + for (Map.Entry<String, ?> entry : backupSp.getAll().entrySet()) { + boolean restored = restoreKey(entry, editor, followUpEditor, backupSp); + if (!restored) { + shouldScheduleJob = true; + } + } + + editor.apply(); + followUpEditor.apply(); + SharedPreferencesHelper.clear(backupSp); + + // If any of the widgets is not yet available, schedule a follow-up job to check later. + if (shouldScheduleJob) { + if (DEBUG) Log.d(TAG, "At least one shortcut is not available, scheduling follow-up."); + PeopleBackupFollowUpJob.scheduleJob(mContext); + } + + updateWidgets(mContext); + } + + /** Backs up an entry from default file to backup file. */ + public void backupKey(Map.Entry<String, ?> entry, SharedPreferences.Editor backupEditor, + List<String> existingWidgets) { + String key = entry.getKey(); + if (TextUtils.isEmpty(key)) { + return; + } + + SharedFileEntryType entryType = getEntryType(entry); + switch(entryType) { + case WIDGET_ID: + backupWidgetIdKey(key, String.valueOf(entry.getValue()), backupEditor, + existingWidgets); + break; + case PEOPLE_TILE_KEY: + backupPeopleTileKey(key, (Set<String>) entry.getValue(), backupEditor, + existingWidgets); + break; + case CONTACT_URI: + backupContactUriKey(key, (Set<String>) entry.getValue(), backupEditor); + break; + case UNKNOWN: + default: + Log.w(TAG, "Key not identified, skipping: " + key); + } + } + + /** + * Tries to restore an entry from backup file to default file. + * Returns true if restore is finished, false if it needs to be checked later. + */ + boolean restoreKey(Map.Entry<String, ?> entry, SharedPreferences.Editor editor, + SharedPreferences.Editor followUpEditor, SharedPreferences backupSp) { + String key = entry.getKey(); + SharedFileEntryType keyType = getEntryType(entry); + int storedUserId = backupSp.getInt(ADD_USER_ID_TO_URI + key, INVALID_USER_ID); + switch (keyType) { + case WIDGET_ID: + restoreWidgetIdKey(key, String.valueOf(entry.getValue()), editor, storedUserId); + return true; + case PEOPLE_TILE_KEY: + return restorePeopleTileKeyAndCorrespondingWidgetFile( + key, (Set<String>) entry.getValue(), editor, followUpEditor); + case CONTACT_URI: + restoreContactUriKey(key, (Set<String>) entry.getValue(), editor, storedUserId); + return true; + case UNKNOWN: + default: + Log.e(TAG, "Key not identified, skipping:" + key); + return true; + } + } + + /** + * Backs up a [widgetId, contactURI] pair, if widget id corresponds to current user. + * If contact URI has a user id, stores it so it can be re-added on restore. + */ + private void backupWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor, + List<String> existingWidgets) { + if (!existingWidgets.contains(key)) { + if (DEBUG) Log.d(TAG, "Widget: " + key + " does't correspond to this user, skipping."); + return; + } + Uri uri = Uri.parse(uriString); + if (ContentProvider.uriHasUserId(uri)) { + if (DEBUG) Log.d(TAG, "Contact URI value has user ID, removing from: " + uri); + int userId = ContentProvider.getUserIdFromUri(uri); + editor.putInt(ADD_USER_ID_TO_URI + key, userId); + uri = ContentProvider.getUriWithoutUserId(uri); + } + if (DEBUG) Log.d(TAG, "Backing up widgetId key: " + key + " . Value: " + uri.toString()); + editor.putString(key, uri.toString()); + } + + /** Restores a [widgetId, contactURI] pair, and a potential {@code storedUserId}. */ + private void restoreWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor, + int storedUserId) { + Uri uri = Uri.parse(uriString); + if (storedUserId != INVALID_USER_ID) { + uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId)); + if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri); + + } + if (DEBUG) Log.d(TAG, "Restoring widgetId key: " + key + " . Value: " + uri.toString()); + editor.putString(key, uri.toString()); + } + + /** + * Backs up a [PeopleTileKey, {widgetIds}] pair, if PeopleTileKey's user is the same as current + * user, stripping out the user id. + */ + private void backupPeopleTileKey(String key, Set<String> widgetIds, + SharedPreferences.Editor editor, List<String> existingWidgets) { + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); + if (peopleTileKey.getUserId() != mUserHandle.getIdentifier()) { + if (DEBUG) Log.d(TAG, "PeopleTileKey corresponds to different user, skipping backup."); + return; + } + + Set<String> filteredWidgets = widgetIds.stream() + .filter(id -> existingWidgets.contains(id)) + .collect(Collectors.toSet()); + if (filteredWidgets.isEmpty()) { + return; + } + + peopleTileKey.setUserId(INVALID_USER_ID); + if (DEBUG) { + Log.d(TAG, "Backing up PeopleTileKey key: " + peopleTileKey.toString() + ". Value: " + + filteredWidgets); + } + editor.putStringSet(peopleTileKey.toString(), filteredWidgets); + } + + /** + * Restores a [PeopleTileKey, {widgetIds}] pair, restoring the user id. Checks if the + * corresponding shortcut exists, and if not, we should schedule a follow up to check later. + * Also restores corresponding [widgetId, PeopleTileKey], which is not backed up since the + * information can be inferred from this. + * Returns true if restore is finished, false if we should check if shortcut is available later. + */ + private boolean restorePeopleTileKeyAndCorrespondingWidgetFile(String key, + Set<String> widgetIds, SharedPreferences.Editor editor, + SharedPreferences.Editor followUpEditor) { + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); + // Should never happen, as type of key has been checked. + if (peopleTileKey == null) { + if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is null, skipping."); + return true; + } + + peopleTileKey.setUserId(mUserHandle.getIdentifier()); + if (!PeopleTileKey.isValid(peopleTileKey)) { + if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is not valid, skipping."); + return true; + } + + boolean restored = isReadyForRestore( + mIPeopleManager, mPackageManager, peopleTileKey); + if (!restored) { + if (DEBUG) Log.d(TAG, "Adding key to follow-up storage: " + peopleTileKey.toString()); + // Follow-up file stores shortcuts that need to be checked later, and possibly wiped + // from our storage. + followUpEditor.putStringSet(peopleTileKey.toString(), widgetIds); + } + + if (DEBUG) { + Log.d(TAG, "Restoring PeopleTileKey key: " + peopleTileKey.toString() + " . Value: " + + widgetIds); + } + editor.putStringSet(peopleTileKey.toString(), widgetIds); + restoreWidgetIdFiles(mContext, widgetIds, peopleTileKey); + return restored; + } + + /** + * Backs up a [contactURI, {widgetIds}] pair. If contactURI contains a userId, we back up + * this entry in the corresponding user. If it doesn't, we back it up as user 0. + * If contact URI has a user id, stores it so it can be re-added on restore. + * We do not take existing widgets for this user into consideration. + */ + private void backupContactUriKey(String key, Set<String> widgetIds, + SharedPreferences.Editor editor) { + Uri uri = Uri.parse(String.valueOf(key)); + if (ContentProvider.uriHasUserId(uri)) { + int userId = ContentProvider.getUserIdFromUri(uri); + if (DEBUG) Log.d(TAG, "Contact URI has user Id: " + userId); + if (userId == mUserHandle.getIdentifier()) { + uri = ContentProvider.getUriWithoutUserId(uri); + if (DEBUG) { + Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: " + + widgetIds); + } + editor.putInt(ADD_USER_ID_TO_URI + uri.toString(), userId); + editor.putStringSet(uri.toString(), widgetIds); + } else { + if (DEBUG) Log.d(TAG, "ContactURI corresponds to different user, skipping."); + } + } else if (mUserHandle.isSystem()) { + if (DEBUG) { + Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: " + + widgetIds); + } + editor.putStringSet(uri.toString(), widgetIds); + } + } + + /** Restores a [contactURI, {widgetIds}] pair, and a potential {@code storedUserId}. */ + private void restoreContactUriKey(String key, Set<String> widgetIds, + SharedPreferences.Editor editor, int storedUserId) { + Uri uri = Uri.parse(key); + if (storedUserId != INVALID_USER_ID) { + uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId)); + if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri); + } + if (DEBUG) { + Log.d(TAG, "Restoring contactURI key: " + uri.toString() + " . Value: " + widgetIds); + } + editor.putStringSet(uri.toString(), widgetIds); + } + + /** Restores the widget-specific files that contain PeopleTileKey information. */ + public static void restoreWidgetIdFiles(Context context, Set<String> widgetIds, + PeopleTileKey key) { + for (String id : widgetIds) { + if (DEBUG) Log.d(TAG, "Restoring widget Id file: " + id + " . Value: " + key); + SharedPreferences dest = context.getSharedPreferences(id, Context.MODE_PRIVATE); + SharedPreferencesHelper.setPeopleTileKey(dest, key); + } + } + + private List<String> getExistingWidgetsForUser(int userId) { + List<String> existingWidgets = new ArrayList<>(); + int[] ids = mAppWidgetManager.getAppWidgetIds( + new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); + for (int id : ids) { + String idString = String.valueOf(id); + SharedPreferences sp = mContext.getSharedPreferences(idString, Context.MODE_PRIVATE); + if (sp.getInt(USER_ID, INVALID_USER_ID) == userId) { + existingWidgets.add(idString); + } + } + if (DEBUG) Log.d(TAG, "Existing widgets: " + existingWidgets); + return existingWidgets; + } + + /** + * Returns whether {@code key} corresponds to a shortcut that is ready for restore, either + * because it is available or because it never will be. If not ready, we schedule a job to check + * again later. + */ + public static boolean isReadyForRestore(IPeopleManager peopleManager, + PackageManager packageManager, PeopleTileKey key) { + if (DEBUG) Log.d(TAG, "Checking if we should schedule a follow up job : " + key); + if (!PeopleTileKey.isValid(key)) { + if (DEBUG) Log.d(TAG, "Key is invalid, should not follow up."); + return true; + } + + try { + PackageInfo info = packageManager.getPackageInfoAsUser( + key.getPackageName(), 0, key.getUserId()); + } catch (PackageManager.NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Package is not installed, should follow up."); + return false; + } + + try { + boolean isConversation = peopleManager.isConversation( + key.getPackageName(), key.getUserId(), key.getShortcutId()); + if (DEBUG) { + Log.d(TAG, "Checked if shortcut exists, should follow up: " + !isConversation); + } + return isConversation; + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "Error checking if backed up info is a shortcut."); + return false; + } + } + + /** Parses default file {@code entry} to determine the entry's type.*/ + public static SharedFileEntryType getEntryType(Map.Entry<String, ?> entry) { + String key = entry.getKey(); + if (key == null) { + return SharedFileEntryType.UNKNOWN; + } + + try { + int id = Integer.parseInt(key); + try { + String contactUri = (String) entry.getValue(); + } catch (Exception e) { + Log.w(TAG, "Malformed value, skipping:" + entry.getValue()); + return SharedFileEntryType.UNKNOWN; + } + return SharedFileEntryType.WIDGET_ID; + } catch (NumberFormatException ignored) { } + + try { + Set<String> widgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.w(TAG, "Malformed value, skipping:" + entry.getValue()); + return SharedFileEntryType.UNKNOWN; + } + + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); + if (peopleTileKey != null) { + return SharedFileEntryType.PEOPLE_TILE_KEY; + } + + try { + Uri uri = Uri.parse(key); + return SharedFileEntryType.CONTACT_URI; + } catch (Exception e) { + return SharedFileEntryType.UNKNOWN; + } + } + + /** Sends a broadcast to update the existing Conversation widgets. */ + public static void updateWidgets(Context context) { + int[] widgetIds = AppWidgetManager.getInstance(context) + .getAppWidgetIds(new ComponentName(context, PeopleSpaceWidgetProvider.class)); + if (DEBUG) { + for (int id : widgetIds) { + Log.d(TAG, "Calling update to widget: " + id); + } + } + if (widgetIds != null && widgetIds.length != 0) { + Intent intent = new Intent(context, PeopleSpaceWidgetProvider.class); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds); + context.sendBroadcast(intent); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 4085df9a8093..3320fbda6fe5 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.content.Intent.ACTION_BOOT_COMPLETED; +import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; @@ -29,6 +30,7 @@ import static com.android.systemui.people.NotificationHelper.getContactUri; import static com.android.systemui.people.NotificationHelper.getHighestPriorityNotification; import static com.android.systemui.people.NotificationHelper.shouldFilterOut; import static com.android.systemui.people.NotificationHelper.shouldMatchNotificationByUri; +import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP; import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; @@ -38,6 +40,7 @@ import static com.android.systemui.people.PeopleSpaceUtils.augmentTileFromNotifi import static com.android.systemui.people.PeopleSpaceUtils.getMessagesCount; import static com.android.systemui.people.PeopleSpaceUtils.getNotificationsByUri; import static com.android.systemui.people.PeopleSpaceUtils.removeNotificationFields; +import static com.android.systemui.people.widget.PeopleBackupHelper.getEntryType; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +49,8 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; +import android.app.backup.BackupManager; +import android.app.job.JobScheduler; import android.app.people.ConversationChannel; import android.app.people.IPeopleManager; import android.app.people.PeopleManager; @@ -84,8 +89,10 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.people.NotificationHelper; +import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; import com.android.systemui.people.PeopleTileViewHelper; +import com.android.systemui.people.SharedPreferencesHelper; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -93,11 +100,13 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; @@ -126,6 +135,7 @@ public class PeopleSpaceWidgetManager { private Optional<Bubbles> mBubblesOptional; private UserManager mUserManager; private PeopleSpaceWidgetManager mManager; + private BackupManager mBackupManager; public UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); private NotificationManager mNotificationManager; private BroadcastDispatcher mBroadcastDispatcher; @@ -164,6 +174,7 @@ public class PeopleSpaceWidgetManager { ServiceManager.getService(Context.NOTIFICATION_SERVICE)); mBubblesOptional = bubblesOptional; mUserManager = userManager; + mBackupManager = new BackupManager(context); mNotificationManager = notificationManager; mManager = this; mBroadcastDispatcher = broadcastDispatcher; @@ -189,6 +200,7 @@ public class PeopleSpaceWidgetManager { null /* executor */, UserHandle.ALL); IntentFilter perAppFilter = new IntentFilter(ACTION_PACKAGE_REMOVED); + perAppFilter.addAction(ACTION_PACKAGE_ADDED); perAppFilter.addDataScheme("package"); // BroadcastDispatcher doesn't allow data schemes. mContext.registerReceiver(mBaseBroadcastReceiver, perAppFilter); @@ -224,7 +236,7 @@ public class PeopleSpaceWidgetManager { AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager, PeopleManager peopleManager, LauncherApps launcherApps, NotificationEntryManager notificationEntryManager, PackageManager packageManager, - Optional<Bubbles> bubblesOptional, UserManager userManager, + Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, @Background Executor executor) { mContext = context; @@ -236,6 +248,7 @@ public class PeopleSpaceWidgetManager { mPackageManager = packageManager; mBubblesOptional = bubblesOptional; mUserManager = userManager; + mBackupManager = backupManager; mINotificationManager = iNotificationManager; mNotificationManager = notificationManager; mManager = this; @@ -257,8 +270,6 @@ public class PeopleSpaceWidgetManager { if (DEBUG) Log.d(TAG, "no widgets to update"); return; } - - if (DEBUG) Log.d(TAG, "updating " + widgetIds.length + " widgets: " + widgetIds); synchronized (mLock) { updateSingleConversationWidgets(widgetIds); } @@ -274,6 +285,7 @@ public class PeopleSpaceWidgetManager { public void updateSingleConversationWidgets(int[] appWidgetIds) { Map<Integer, PeopleSpaceTile> widgetIdToTile = new HashMap<>(); for (int appWidgetId : appWidgetIds) { + if (DEBUG) Log.d(TAG, "Updating widget: " + appWidgetId); PeopleSpaceTile tile = getTileForExistingWidget(appWidgetId); if (tile == null) { Log.e(TAG, "Matching conversation not found for shortcut ID"); @@ -293,14 +305,16 @@ public class PeopleSpaceWidgetManager { private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) { PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId); if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString()); - if (!key.isValid()) { + + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Cannot update invalid widget"); return; } - RemoteViews views = new PeopleTileViewHelper(mContext, tile, appWidgetId, - options, key).getViews(); + RemoteViews views = PeopleTileViewHelper.createRemoteViews(mContext, tile, appWidgetId, + options, key); // Tell the AppWidgetManager to perform an update on the current app widget. + if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId); mAppWidgetManager.updateAppWidget(appWidgetId, views); } @@ -314,6 +328,9 @@ public class PeopleSpaceWidgetManager { /** Updates tile in app widget options and the current view. */ public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) { + if (tile == null) { + if (DEBUG) Log.w(TAG, "Storing null tile"); + } synchronized (mTiles) { mTiles.put(appWidgetId, tile); } @@ -368,7 +385,7 @@ public class PeopleSpaceWidgetManager { @Nullable public PeopleSpaceTile getTileFromPersistentStorage(PeopleTileKey key, int appWidgetId) throws PackageManager.NameNotFoundException { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "PeopleTileKey invalid: " + key.toString()); return null; } @@ -382,7 +399,7 @@ public class PeopleSpaceWidgetManager { ConversationChannel channel = mIPeopleManager.getConversation( key.getPackageName(), key.getUserId(), key.getShortcutId()); if (channel == null) { - Log.d(TAG, "Could not retrieve conversation from storage"); + if (DEBUG) Log.d(TAG, "Could not retrieve conversation from storage"); return null; } @@ -430,7 +447,8 @@ public class PeopleSpaceWidgetManager { try { PeopleTileKey key = new PeopleTileKey( sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName()); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { + Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString()); return; } int[] widgetIds = mAppWidgetManager.getAppWidgetIds( @@ -561,14 +579,14 @@ public class PeopleSpaceWidgetManager { if (DEBUG) Log.d(TAG, "Augmenting tile from notification, key: " + key.toString()); return augmentTileFromNotification(mContext, tile, key, highestPriority, messagesCount, - appWidgetId); + appWidgetId, mBackupManager); } /** Returns an augmented tile for an existing widget. */ @Nullable public Optional<PeopleSpaceTile> getAugmentedTileForExistingWidget(int widgetId, Map<PeopleTileKey, Set<NotificationEntry>> notifications) { - Log.d(TAG, "Augmenting tile for existing widget: " + widgetId); + if (DEBUG) Log.d(TAG, "Augmenting tile for existing widget: " + widgetId); PeopleSpaceTile tile = getTileForExistingWidget(widgetId); if (tile == null) { if (DEBUG) { @@ -588,7 +606,7 @@ public class PeopleSpaceWidgetManager { /** Returns stored widgets for the conversation specified. */ public Set<String> getMatchingKeyWidgetIds(PeopleTileKey key) { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { return new HashSet<>(); } return new HashSet<>(mSharedPrefs.getStringSet(key.toString(), new HashSet<>())); @@ -776,7 +794,7 @@ public class PeopleSpaceWidgetManager { // PeopleTileKey arguments. if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId); PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions); - if (optionsKey.isValid()) { + if (PeopleTileKey.isValid(optionsKey)) { if (DEBUG) { Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: " + optionsKey.getShortcutId()); @@ -808,7 +826,7 @@ public class PeopleSpaceWidgetManager { existingKeyIfStored = getKeyFromStorageByWidgetId(appWidgetId); } // Delete previous storage if the widget already existed and is just reconfigured. - if (existingKeyIfStored.isValid()) { + if (PeopleTileKey.isValid(existingKeyIfStored)) { if (DEBUG) Log.d(TAG, "Remove previous storage for widget: " + appWidgetId); deleteWidgets(new int[]{appWidgetId}); } else { @@ -820,7 +838,7 @@ public class PeopleSpaceWidgetManager { synchronized (mLock) { if (DEBUG) Log.d(TAG, "Add storage for : " + key.toString()); PeopleSpaceUtils.setSharedPreferencesStorageForTile(mContext, key, appWidgetId, - tile.getContactUri()); + tile.getContactUri(), mBackupManager); } if (DEBUG) Log.d(TAG, "Ensure listener is registered for widget: " + appWidgetId); registerConversationListenerIfNeeded(appWidgetId, key); @@ -838,7 +856,7 @@ public class PeopleSpaceWidgetManager { /** Registers a conversation listener for {@code appWidgetId} if not already registered. */ public void registerConversationListenerIfNeeded(int widgetId, PeopleTileKey key) { // Retrieve storage needed for registration. - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.w(TAG, "Could not register listener for widget: " + widgetId); return; } @@ -887,7 +905,7 @@ public class PeopleSpaceWidgetManager { widgetSp.getString(SHORTCUT_ID, null), widgetSp.getInt(USER_ID, INVALID_USER_ID), widgetSp.getString(PACKAGE_NAME, null)); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.e(TAG, "Could not delete " + widgetId); return; } @@ -1031,8 +1049,8 @@ public class PeopleSpaceWidgetManager { Optional.empty()); if (DEBUG) Log.i(TAG, "Returning tile preview for shortcutId: " + shortcutId); - return new PeopleTileViewHelper(mContext, augmentedTile, 0, options, - new PeopleTileKey(augmentedTile)).getViews(); + return PeopleTileViewHelper.createRemoteViews(mContext, augmentedTile, 0, options, + new PeopleTileKey(augmentedTile)); } protected final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() { @@ -1053,6 +1071,7 @@ public class PeopleSpaceWidgetManager { return; } for (int appWidgetId : appWidgetIds) { + if (DEBUG) Log.d(TAG, "Updating widget from broadcast, widget id: " + appWidgetId); PeopleSpaceTile existingTile = null; PeopleSpaceTile updatedTile = null; try { @@ -1060,7 +1079,7 @@ public class PeopleSpaceWidgetManager { existingTile = getTileForExistingWidgetThrowing(appWidgetId); if (existingTile == null) { Log.e(TAG, "Matching conversation not found for shortcut ID"); - return; + continue; } updatedTile = getTileWithCurrentState(existingTile, entryPoint); updateAppWidgetOptionsAndView(appWidgetId, updatedTile); @@ -1068,6 +1087,14 @@ public class PeopleSpaceWidgetManager { } catch (PackageManager.NameNotFoundException e) { // Delete data for uninstalled widgets. Log.e(TAG, "Package no longer found for tile: " + e); + JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); + if (jobScheduler != null + && jobScheduler.getPendingJob(PeopleBackupFollowUpJob.JOB_ID) != null) { + if (DEBUG) { + Log.d(TAG, "Device was recently restored, wait before deleting storage."); + } + continue; + } synchronized (mLock) { updateAppWidgetOptionsAndView(appWidgetId, updatedTile); } @@ -1185,4 +1212,149 @@ public class PeopleSpaceWidgetManager { return PeopleSpaceTile.BLOCK_CONVERSATIONS; } } + + /** + * Modifies widgets storage after a restore operation, since widget ids get remapped on restore. + * This is guaranteed to run after the PeopleBackupHelper restore operation. + */ + public void remapWidgets(int[] oldWidgetIds, int[] newWidgetIds) { + if (DEBUG) { + Log.d(TAG, "Remapping widgets, old: " + Arrays.toString(oldWidgetIds) + ". new: " + + Arrays.toString(newWidgetIds)); + } + + Map<String, String> widgets = new HashMap<>(); + for (int i = 0; i < oldWidgetIds.length; i++) { + widgets.put(String.valueOf(oldWidgetIds[i]), String.valueOf(newWidgetIds[i])); + } + + remapWidgetFiles(widgets); + remapSharedFile(widgets); + remapFollowupFile(widgets); + + int[] widgetIds = mAppWidgetManager.getAppWidgetIds( + new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); + Bundle b = new Bundle(); + b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true); + for (int id : widgetIds) { + if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id); + mAppWidgetManager.updateAppWidgetOptions(id, b); + } + + updateWidgets(widgetIds); + } + + /** Remaps widget ids in widget specific files. */ + public void remapWidgetFiles(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping widget files"); + Map<String, PeopleTileKey> remapped = new HashMap<>(); + for (Map.Entry<String, String> entry : widgets.entrySet()) { + String from = String.valueOf(entry.getKey()); + String to = String.valueOf(entry.getValue()); + if (Objects.equals(from, to)) { + continue; + } + + SharedPreferences src = mContext.getSharedPreferences(from, Context.MODE_PRIVATE); + PeopleTileKey key = SharedPreferencesHelper.getPeopleTileKey(src); + if (PeopleTileKey.isValid(key)) { + if (DEBUG) { + Log.d(TAG, "Moving PeopleTileKey: " + key.toString() + " from file: " + + from + ", to file: " + to); + } + remapped.put(to, key); + SharedPreferencesHelper.clear(src); + } else { + if (DEBUG) Log.d(TAG, "Widget file has invalid key: " + key); + } + } + for (Map.Entry<String, PeopleTileKey> entry : remapped.entrySet()) { + SharedPreferences dest = mContext.getSharedPreferences( + entry.getKey(), Context.MODE_PRIVATE); + SharedPreferencesHelper.setPeopleTileKey(dest, entry.getValue()); + } + } + + /** Remaps widget ids in default shared storage. */ + public void remapSharedFile(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping shared file"); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + SharedPreferences.Editor editor = sp.edit(); + Map<String, ?> all = sp.getAll(); + for (Map.Entry<String, ?> entry : all.entrySet()) { + String key = entry.getKey(); + PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry); + if (DEBUG) Log.d(TAG, "Remapping key:" + key); + switch (keyType) { + case WIDGET_ID: + String newId = widgets.get(key); + if (TextUtils.isEmpty(newId)) { + Log.w(TAG, "Key is widget id without matching new id, skipping: " + key); + break; + } + if (DEBUG) Log.d(TAG, "Key is widget id: " + key + ", replace with: " + newId); + try { + editor.putString(newId, (String) entry.getValue()); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + } + editor.remove(key); + break; + case PEOPLE_TILE_KEY: + case CONTACT_URI: + Set<String> oldWidgetIds; + try { + oldWidgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + editor.remove(key); + break; + } + Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets); + if (DEBUG) { + Log.d(TAG, "Key is PeopleTileKey or contact URI: " + key + + ", replace values with new ids: " + newWidgets); + } + editor.putStringSet(key, newWidgets); + break; + case UNKNOWN: + Log.e(TAG, "Key not identified:" + key); + } + } + editor.apply(); + } + + /** Remaps widget ids in follow-up job file. */ + public void remapFollowupFile(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping follow up file"); + SharedPreferences followUp = mContext.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + Map<String, ?> followUpAll = followUp.getAll(); + for (Map.Entry<String, ?> entry : followUpAll.entrySet()) { + String key = entry.getKey(); + Set<String> oldWidgetIds; + try { + oldWidgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + followUpEditor.remove(key); + continue; + } + Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets); + if (DEBUG) { + Log.d(TAG, "Follow up key: " + key + ", replace with new ids: " + newWidgets); + } + followUpEditor.putStringSet(key, newWidgets); + } + followUpEditor.apply(); + } + + private Set<String> getNewWidgets(Set<String> oldWidgets, Map<String, String> widgetsMapping) { + return oldWidgets + .stream() + .map(widgetsMapping::get) + .filter(id -> !TextUtils.isEmpty(id)) + .collect(Collectors.toSet()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java index a28da43a80b6..c4be197504be 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java @@ -75,7 +75,7 @@ public class PeopleSpaceWidgetPinnedReceiver extends BroadcastReceiver { String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, INVALID_USER_ID); PeopleTileKey key = new PeopleTileKey(shortcutId, userId, packageName); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.w(TAG, "Skipping: key is not valid: " + key.toString()); return; } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java index 3522b76e6460..36939b735a07 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java @@ -70,6 +70,13 @@ public class PeopleSpaceWidgetProvider extends AppWidgetProvider { mPeopleSpaceWidgetManager.deleteWidgets(appWidgetIds); } + @Override + public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + super.onRestored(context, oldWidgetIds, newWidgetIds); + ensurePeopleSpaceWidgetManagerInitialized(); + mPeopleSpaceWidgetManager.remapWidgets(oldWidgetIds, newWidgetIds); + } + private void ensurePeopleSpaceWidgetManagerInitialized() { mPeopleSpaceWidgetManager.init(); } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java index 319df85b4872..6e6ca254dee0 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java @@ -25,6 +25,8 @@ import android.text.TextUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** Class that encapsulates fields identifying a Conversation. */ public class PeopleTileKey { @@ -32,6 +34,8 @@ public class PeopleTileKey { private int mUserId; private String mPackageName; + private static final Pattern KEY_PATTERN = Pattern.compile("(.+)/(-?\\d+)/(\\p{L}.*)"); + public PeopleTileKey(String shortcutId, int userId, String packageName) { mShortcutId = shortcutId; mUserId = userId; @@ -66,8 +70,12 @@ public class PeopleTileKey { return mPackageName; } + public void setUserId(int userId) { + mUserId = userId; + } + /** Returns whether PeopleTileKey is valid/well-formed. */ - public boolean isValid() { + private boolean validate() { return !TextUtils.isEmpty(mShortcutId) && !TextUtils.isEmpty(mPackageName) && mUserId >= 0; } @@ -88,10 +96,31 @@ public class PeopleTileKey { */ @Override public String toString() { - if (!isValid()) return EMPTY_STRING; return mShortcutId + "/" + mUserId + "/" + mPackageName; } + /** Parses {@code key} into a {@link PeopleTileKey}. */ + public static PeopleTileKey fromString(String key) { + if (key == null) { + return null; + } + Matcher m = KEY_PATTERN.matcher(key); + if (m.find()) { + try { + int userId = Integer.parseInt(m.group(2)); + return new PeopleTileKey(m.group(1), userId, m.group(3)); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** Returns whether {@code key} is a valid {@link PeopleTileKey}. */ + public static boolean isValid(PeopleTileKey key) { + return key != null && key.validate(); + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java b/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java index 1ed98c0a8f90..03d1f15bf379 100644 --- a/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java +++ b/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java @@ -93,6 +93,8 @@ public class InattentiveSleepWarningView extends FrameLayout { setAlpha(1f); setVisibility(View.VISIBLE); mWindowManager.addView(this, getLayoutParams(mWindowToken)); + announceForAccessibility( + getContext().getString(R.string.inattentive_sleep_warning_message)); } /** diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java b/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java new file mode 100644 index 000000000000..e5479badcb0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2021 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.privacy.television; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.R; + +/** + * Drawable that can go from being the background of the privacy icons to a small dot. + * The icons are not included. + */ +public class PrivacyChipDrawable extends Drawable { + + private static final String TAG = PrivacyChipDrawable.class.getSimpleName(); + private static final boolean DEBUG = false; + + private float mWidth; + private float mHeight; + private float mMarginEnd; + private float mRadius; + private int mDotAlpha; + private int mBgAlpha; + + private float mTargetWidth; + private final int mMinWidth; + private final int mIconWidth; + private final int mIconPadding; + private final int mBgWidth; + private final int mBgHeight; + private final int mBgRadius; + private final int mDotSize; + + private final AnimatorSet mFadeIn; + private final AnimatorSet mFadeOut; + private final AnimatorSet mCollapse; + private final AnimatorSet mExpand; + private Animator mWidthAnimator; + + private final Paint mChipPaint; + private final Paint mBgPaint; + + private boolean mIsRtl; + + private boolean mIsExpanded = true; + + private PrivacyChipDrawableListener mListener; + + interface PrivacyChipDrawableListener { + void onFadeOutFinished(); + } + + public PrivacyChipDrawable(Context context) { + mChipPaint = new Paint(); + mChipPaint.setStyle(Paint.Style.FILL); + mChipPaint.setColor(context.getColor(R.color.privacy_circle)); + mChipPaint.setAlpha(mDotAlpha); + mChipPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + + mBgPaint = new Paint(); + mBgPaint.setStyle(Paint.Style.FILL); + mBgPaint.setColor(context.getColor(R.color.privacy_chip_dot_bg_tint)); + mBgPaint.setAlpha(mBgAlpha); + mBgPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + + mBgWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_width); + mBgHeight = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_dot_bg_height); + mBgRadius = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_dot_bg_radius); + + mMinWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_min_width); + mIconWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_icon_size); + mIconPadding = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_margin_in_between); + mDotSize = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_dot_size); + + mWidth = mMinWidth; + mHeight = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_height); + mRadius = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_radius); + + mExpand = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_expand); + mExpand.setTarget(this); + + mCollapse = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_collapse); + mCollapse.setTarget(this); + + mFadeIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_fade_in); + mFadeIn.setTarget(this); + + mFadeOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_fade_out); + mFadeOut.setTarget(this); + mFadeOut.addListener(new Animator.AnimatorListener() { + private boolean mCancelled; + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelled && mListener != null) { + if (DEBUG) Log.d(TAG, "Fade-out complete"); + mListener.onFadeOutFinished(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationRepeat(Animator animation) { + // no-op + } + }); + } + + /** + * Pass null to remove listener. + */ + public void setListener(@Nullable PrivacyChipDrawableListener listener) { + this.mListener = listener; + } + + /** + * Call once the view that is showing the drawable is visible to start fading the chip in. + */ + public void startInitialFadeIn() { + if (DEBUG) Log.d(TAG, "initial fade-in"); + mFadeIn.start(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + Rect bounds = getBounds(); + + int centerVertical = (bounds.bottom - bounds.top) / 2; + // Dot background + RectF bgBounds = new RectF( + mIsRtl ? bounds.left : bounds.right - mBgWidth, + centerVertical - mBgHeight / 2f, + mIsRtl ? bounds.left + mBgWidth : bounds.right, + centerVertical + mBgHeight / 2f); + if (DEBUG) Log.v(TAG, "bg: " + bgBounds.toShortString()); + canvas.drawRoundRect(bgBounds, mBgRadius, mBgRadius, mBgPaint); + + // Icon background / dot + RectF greenBounds = new RectF( + mIsRtl ? bounds.left + mMarginEnd : bounds.right - mWidth - mMarginEnd, + centerVertical - mHeight / 2, + mIsRtl ? bounds.left + mWidth + mMarginEnd : bounds.right - mMarginEnd, + centerVertical + mHeight / 2); + if (DEBUG) Log.v(TAG, "green: " + greenBounds.toShortString()); + canvas.drawRoundRect(greenBounds, mRadius, mRadius, mChipPaint); + } + + private void animateToNewTargetWidth(float width) { + if (DEBUG) Log.d(TAG, "new target width: " + width); + if (width != mTargetWidth) { + mTargetWidth = width; + Animator newWidthAnimator = ObjectAnimator.ofFloat(this, "width", mTargetWidth); + newWidthAnimator.start(); + if (mWidthAnimator != null) { + mWidthAnimator.cancel(); + } + mWidthAnimator = newWidthAnimator; + } + } + + private void expand() { + if (DEBUG) Log.d(TAG, "expanding"); + if (mIsExpanded) { + return; + } + mIsExpanded = true; + + mExpand.start(); + mCollapse.cancel(); + } + + /** + * Starts the animation to a dot. + */ + public void collapse() { + if (DEBUG) Log.d(TAG, "collapsing"); + if (!mIsExpanded) { + return; + } + mIsExpanded = false; + + animateToNewTargetWidth(mDotSize); + mCollapse.start(); + mExpand.cancel(); + } + + /** + * Fades out the view if 0 icons are to be shown, expands the chip if it has been collapsed and + * makes the width of the chip adjust to the amount of icons to be shown. + * Should not be called when only the order of the icons was changed as the chip will expand + * again without there being any real update. + * + * @param iconCount Can be 0 to fade out the chip. + */ + public void updateIcons(int iconCount) { + if (DEBUG) Log.d(TAG, "updating icons: " + iconCount); + + // calculate chip size and use it for end value of animation that is specified in code, + // not xml + if (iconCount == 0) { + // fade out if there are no icons + mFadeOut.start(); + + mWidthAnimator.cancel(); + mFadeIn.cancel(); + mExpand.cancel(); + mCollapse.cancel(); + return; + } + + mFadeOut.cancel(); + expand(); + animateToNewTargetWidth(mMinWidth + (iconCount - 1) * (mIconWidth + mIconPadding)); + } + + @Override + public void setAlpha(int alpha) { + setDotAlpha(alpha); + setBgAlpha(alpha); + } + + @Override + public int getAlpha() { + return mDotAlpha; + } + + /** + * Set alpha value the green part of the chip. + */ + @Keep + public void setDotAlpha(int alpha) { + if (DEBUG) Log.v(TAG, "dot alpha updated to: " + alpha); + mDotAlpha = alpha; + mChipPaint.setAlpha(alpha); + } + + @Keep + public int getDotAlpha() { + return mDotAlpha; + } + + /** + * Set alpha value of the background of the chip. + */ + @Keep + public void setBgAlpha(int alpha) { + if (DEBUG) Log.v(TAG, "bg alpha updated to: " + alpha); + mBgAlpha = alpha; + mBgPaint.setAlpha(alpha); + } + + @Keep + public int getBgAlpha() { + return mBgAlpha; + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // no-op + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + /** + * The radius of the green part of the chip, not the background. + */ + @Keep + public void setRadius(float radius) { + mRadius = radius; + invalidateSelf(); + } + + /** + * @return The radius of the green part of the chip, not the background. + */ + @Keep + public float getRadius() { + return mRadius; + } + + /** + * Height of the green part of the chip, not including the background. + */ + @Keep + public void setHeight(float height) { + mHeight = height; + invalidateSelf(); + } + + /** + * @return Height of the green part of the chip, not including the background. + */ + @Keep + public float getHeight() { + return mHeight; + } + + /** + * Width of the green part of the chip, not including the background. + */ + @Keep + public void setWidth(float width) { + mWidth = width; + invalidateSelf(); + } + + /** + * @return Width of the green part of the chip, not including the background. + */ + @Keep + public float getWidth() { + return mWidth; + } + + /** + * Margin at the end of the green part of the chip, so that it will be placed in the middle of + * the rounded rectangle in the background. + */ + @Keep + public void setMarginEnd(float marginEnd) { + mMarginEnd = marginEnd; + invalidateSelf(); + } + + /** + * @return Margin at the end of the green part of the chip, so that it will be placed in the + * middle of the rounded rectangle in the background. + */ + @Keep + public float getMarginEnd() { + return mMarginEnd; + } + + /** + * Sets the layout direction. + */ + public void setRtl(boolean isRtl) { + mIsRtl = isRtl; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java index 5ab7bd88e49b..e4f5cde37f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java +++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java @@ -25,9 +25,10 @@ import android.annotation.IntDef; import android.annotation.UiThread; import android.content.Context; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -38,15 +39,20 @@ import android.view.WindowManager; import android.widget.ImageView; import android.widget.LinearLayout; +import androidx.annotation.NonNull; + import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.privacy.PrivacyChipBuilder; import com.android.systemui.privacy.PrivacyItem; import com.android.systemui.privacy.PrivacyItemController; +import com.android.systemui.privacy.PrivacyType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -56,9 +62,10 @@ import javax.inject.Inject; * recording audio, accessing the camera or accessing the location. */ @SysUISingleton -public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback { +public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback, + PrivacyChipDrawable.PrivacyChipDrawableListener { private static final String TAG = "TvOngoingPrivacyChip"; - static final boolean DEBUG = false; + private static final boolean DEBUG = false; // This title is used in CameraMicIndicatorsPermissionTest and // RecognitionServiceMicIndicatorTest. @@ -68,7 +75,8 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl @IntDef(prefix = {"STATE_"}, value = { STATE_NOT_SHOWN, STATE_APPEARING, - STATE_SHOWN, + STATE_EXPANDED, + STATE_COLLAPSED, STATE_DISAPPEARING }) public @interface State { @@ -76,46 +84,58 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl private static final int STATE_NOT_SHOWN = 0; private static final int STATE_APPEARING = 1; - private static final int STATE_SHOWN = 2; - private static final int STATE_DISAPPEARING = 3; + private static final int STATE_EXPANDED = 2; + private static final int STATE_COLLAPSED = 3; + private static final int STATE_DISAPPEARING = 4; - private static final int ANIMATION_DURATION_MS = 200; + private static final int EXPANDED_DURATION_MS = 4000; + public final int mAnimationDurationMs; private final Context mContext; private final PrivacyItemController mPrivacyItemController; - private View mIndicatorView; + private ViewGroup mIndicatorView; private boolean mViewAndWindowAdded; private ObjectAnimator mAnimator; private boolean mMicCameraIndicatorFlagEnabled; - private boolean mLocationIndicatorEnabled; - private List<PrivacyItem> mPrivacyItems; + private boolean mAllIndicatorsEnabled; + + @NonNull + private List<PrivacyItem> mPrivacyItems = Collections.emptyList(); private LinearLayout mIconsContainer; private final int mIconSize; private final int mIconMarginStart; + private PrivacyChipDrawable mChipDrawable; + + private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper()); + private final Runnable mCollapseRunnable = this::collapseChip; + @State private int mState = STATE_NOT_SHOWN; @Inject public TvOngoingPrivacyChip(Context context, PrivacyItemController privacyItemController) { super(context); - Log.d(TAG, "Privacy chip running without id"); + if (DEBUG) Log.d(TAG, "Privacy chip running"); mContext = context; mPrivacyItemController = privacyItemController; Resources res = mContext.getResources(); - mIconMarginStart = Math.round(res.getDimension(R.dimen.privacy_chip_icon_margin)); + mIconMarginStart = Math.round( + res.getDimension(R.dimen.privacy_chip_icon_margin_in_between)); mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size); + mAnimationDurationMs = res.getInteger(R.integer.privacy_chip_animation_millis); + mMicCameraIndicatorFlagEnabled = privacyItemController.getMicCameraAvailable(); - mLocationIndicatorEnabled = privacyItemController.getLocationAvailable(); + mAllIndicatorsEnabled = privacyItemController.getAllIndicatorsAvailable(); if (DEBUG) { Log.d(TAG, "micCameraIndicators: " + mMicCameraIndicatorFlagEnabled); - Log.d(TAG, "locationIndicators: " + mLocationIndicatorEnabled); + Log.d(TAG, "allIndicators: " + mAllIndicatorsEnabled); } } @@ -125,69 +145,145 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl } @Override - public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) { + public void onPrivacyItemsChanged(@NonNull List<PrivacyItem> privacyItems) { if (DEBUG) Log.d(TAG, "PrivacyItemsChanged"); - mPrivacyItems = privacyItems; - updateUI(); + + List<PrivacyItem> updatedPrivacyItems = new ArrayList<>(privacyItems); + // Never show the location indicator on tv. + if (updatedPrivacyItems.removeIf( + privacyItem -> privacyItem.getPrivacyType() == PrivacyType.TYPE_LOCATION)) { + if (DEBUG) Log.v(TAG, "Removed the location item"); + } + + if (isChipDisabled()) { + fadeOutIndicator(); + mPrivacyItems = updatedPrivacyItems; + return; + } + + // Do they have the same elements? (order doesn't matter) + if (updatedPrivacyItems.size() == mPrivacyItems.size() + && mPrivacyItems.containsAll(updatedPrivacyItems)) { + if (DEBUG) Log.d(TAG, "List wasn't updated"); + return; + } + + mPrivacyItems = updatedPrivacyItems; + updateChip(); + } + + private void updateChip() { + if (DEBUG) Log.d(TAG, mPrivacyItems.size() + " privacy items"); + + if (mPrivacyItems.isEmpty()) { + if (DEBUG) Log.d(TAG, "removing indicator (state: " + stateToString(mState) + ")"); + fadeOutIndicator(); + return; + } + + if (DEBUG) Log.d(TAG, "Current state: " + stateToString(mState)); + switch (mState) { + case STATE_NOT_SHOWN: + createAndShowIndicator(); + break; + case STATE_APPEARING: + case STATE_EXPANDED: + updateIcons(); + collapseLater(); + break; + case STATE_COLLAPSED: + case STATE_DISAPPEARING: + mState = STATE_EXPANDED; + updateIcons(); + animateIconAppearance(); + break; + } + } + + /** + * Collapse the chip EXPANDED_DURATION_MS from now. + */ + private void collapseLater() { + mUiThreadHandler.removeCallbacks(mCollapseRunnable); + if (DEBUG) Log.d(TAG, "chip will collapse in " + EXPANDED_DURATION_MS + "ms"); + mUiThreadHandler.postDelayed(mCollapseRunnable, EXPANDED_DURATION_MS); + } + + private void collapseChip() { + if (DEBUG) Log.d(TAG, "collapseChip"); + + if (mState != STATE_EXPANDED) { + return; + } + mState = STATE_COLLAPSED; + + if (mChipDrawable != null) { + mChipDrawable.collapse(); + } + animateIconDisappearance(); } @Override public void onFlagMicCameraChanged(boolean flag) { if (DEBUG) Log.d(TAG, "mic/camera indicators enabled: " + flag); mMicCameraIndicatorFlagEnabled = flag; + updateChipOnFlagChanged(); } @Override - public void onFlagLocationChanged(boolean flag) { - if (DEBUG) Log.d(TAG, "location indicators enabled: " + flag); - mLocationIndicatorEnabled = flag; + public void onFlagAllChanged(boolean flag) { + if (DEBUG) Log.d(TAG, "all indicators enabled: " + flag); + mAllIndicatorsEnabled = flag; + updateChipOnFlagChanged(); } - private void updateUI() { - if (DEBUG) Log.d(TAG, mPrivacyItems.size() + " privacy items"); + private boolean isChipDisabled() { + return !(mMicCameraIndicatorFlagEnabled || mAllIndicatorsEnabled); + } - if ((mMicCameraIndicatorFlagEnabled || mLocationIndicatorEnabled) - && !mPrivacyItems.isEmpty()) { - if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) { - showIndicator(); - } else { - if (DEBUG) Log.d(TAG, "only updating icons"); - PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems); - setIcons(builder.generateIcons(), mIconsContainer); - mIconsContainer.requestLayout(); - } + private void updateChipOnFlagChanged() { + if (isChipDisabled()) { + fadeOutIndicator(); } else { - hideIndicatorIfNeeded(); + updateChip(); } } @UiThread - private void hideIndicatorIfNeeded() { + private void fadeOutIndicator() { if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) return; + mUiThreadHandler.removeCallbacks(mCollapseRunnable); + if (mViewAndWindowAdded) { mState = STATE_DISAPPEARING; - animateDisappearance(); + animateIconDisappearance(); } else { // Appearing animation has not started yet, as we were still waiting for the View to be // laid out. mState = STATE_NOT_SHOWN; removeIndicatorView(); } + if (mChipDrawable != null) { + mChipDrawable.updateIcons(0); + } } @UiThread - private void showIndicator() { + private void createAndShowIndicator() { mState = STATE_APPEARING; + if (mIndicatorView != null || mViewAndWindowAdded) { + removeIndicatorView(); + } + // Inflate the indicator view - mIndicatorView = LayoutInflater.from(mContext).inflate( + mIndicatorView = (ViewGroup) LayoutInflater.from(mContext).inflate( R.layout.tv_ongoing_privacy_chip, null); - // 1. Set alpha to 0. + // 1. Set icon alpha to 0. // 2. Wait until the window is shown and the view is laid out. // 3. Start a "fade in" (alpha) animation. - mIndicatorView.setAlpha(0f); mIndicatorView .getViewTreeObserver() .addOnGlobalLayoutListener( @@ -196,20 +292,35 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl public void onGlobalLayout() { // State could have changed to NOT_SHOWN (if all the recorders are // already gone) - if (mState != STATE_APPEARING) return; + if (mState != STATE_APPEARING) { + return; + } mViewAndWindowAdded = true; // Remove the observer mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener( this); - animateAppearance(); + animateIconAppearance(); + mChipDrawable.startInitialFadeIn(); } }); + final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_RTL; + if (DEBUG) Log.d(TAG, "is RTL: " + isRtl); + + mChipDrawable = new PrivacyChipDrawable(mContext); + mChipDrawable.setListener(this); + mChipDrawable.setRtl(isRtl); + ImageView chipBackground = mIndicatorView.findViewById(R.id.chip_drawable); + if (chipBackground != null) { + chipBackground.setImageDrawable(mChipDrawable); + } + mIconsContainer = mIndicatorView.findViewById(R.id.icons_container); - PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems); - setIcons(builder.generateIcons(), mIconsContainer); + mIconsContainer.setAlpha(0f); + updateIcons(); final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( WRAP_CONTENT, @@ -217,19 +328,19 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); - layoutParams.gravity = Gravity.TOP | Gravity.END; + layoutParams.gravity = Gravity.TOP | (isRtl ? Gravity.LEFT : Gravity.RIGHT); layoutParams.setTitle(LAYOUT_PARAMS_TITLE); layoutParams.packageName = mContext.getPackageName(); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.addView(mIndicatorView, layoutParams); - } - private void setIcons(List<Drawable> icons, ViewGroup iconsContainer) { - iconsContainer.removeAllViews(); + private void updateIcons() { + List<Drawable> icons = new PrivacyChipBuilder(mContext, mPrivacyItems).generateIcons(); + mIconsContainer.removeAllViews(); for (int i = 0; i < icons.size(); i++) { Drawable icon = icons.get(i); - icon.mutate().setTint(Color.WHITE); + icon.mutate().setTint(mContext.getColor(R.color.privacy_icon_tint)); ImageView imageView = new ImageView(mContext); imageView.setImageDrawable(icon); imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); @@ -241,22 +352,25 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl imageView.setLayoutParams(layoutParams); } } + if (mChipDrawable != null) { + mChipDrawable.updateIcons(icons.size()); + } } - private void animateAppearance() { - animateAlphaTo(1f); + private void animateIconAppearance() { + animateIconAlphaTo(1f); } - private void animateDisappearance() { - animateAlphaTo(0f); + private void animateIconDisappearance() { + animateIconAlphaTo(0f); } - private void animateAlphaTo(final float endValue) { + private void animateIconAlphaTo(float endValue) { if (mAnimator == null) { if (DEBUG) Log.d(TAG, "set up animator"); mAnimator = new ObjectAnimator(); - mAnimator.setTarget(mIndicatorView); + mAnimator.setTarget(mIconsContainer); mAnimator.setProperty(View.ALPHA); mAnimator.addListener(new AnimatorListenerAdapter() { boolean mCancelled; @@ -280,7 +394,7 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl // and then onAnimationEnd(...). We, however, only want to proceed here if the // animation ended "naturally". if (!mCancelled) { - onAnimationFinished(); + onIconAnimationFinished(); } } }); @@ -289,19 +403,37 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl mAnimator.cancel(); } - final float currentValue = mIndicatorView.getAlpha(); + final float currentValue = mIconsContainer.getAlpha(); + if (currentValue == endValue) { + if (DEBUG) Log.d(TAG, "alpha not changing"); + return; + } if (DEBUG) Log.d(TAG, "animate alpha to " + endValue + " from " + currentValue); - mAnimator.setDuration((int) (Math.abs(currentValue - endValue) * ANIMATION_DURATION_MS)); + mAnimator.setDuration(mAnimationDurationMs); mAnimator.setFloatValues(endValue); mAnimator.start(); } - private void onAnimationFinished() { - if (DEBUG) Log.d(TAG, "onAnimationFinished"); + @Override + public void onFadeOutFinished() { + if (DEBUG) Log.d(TAG, "drawable fade-out finished"); + + if (mState == STATE_DISAPPEARING) { + removeIndicatorView(); + mState = STATE_NOT_SHOWN; + } + } + + private void onIconAnimationFinished() { + if (DEBUG) Log.d(TAG, "onAnimationFinished (icon fade)"); + + if (mState == STATE_APPEARING || mState == STATE_EXPANDED) { + collapseLater(); + } if (mState == STATE_APPEARING) { - mState = STATE_SHOWN; + mState = STATE_EXPANDED; } else if (mState == STATE_DISAPPEARING) { removeIndicatorView(); mState = STATE_NOT_SHOWN; @@ -312,14 +444,39 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl if (DEBUG) Log.d(TAG, "removeIndicatorView"); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - if (windowManager != null) { + if (windowManager != null && mIndicatorView != null) { windowManager.removeView(mIndicatorView); } mIndicatorView = null; mAnimator = null; + if (mChipDrawable != null) { + mChipDrawable.setListener(null); + mChipDrawable = null; + } + mViewAndWindowAdded = false; } + /** + * Used in debug logs. + */ + private String stateToString(@State int state) { + switch (state) { + case STATE_NOT_SHOWN: + return "NOT_SHOWN"; + case STATE_APPEARING: + return "APPEARING"; + case STATE_EXPANDED: + return "EXPANDED"; + case STATE_COLLAPSED: + return "COLLAPSED"; + case STATE_DISAPPEARING: + return "DISAPPEARING"; + default: + return "INVALID"; + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index 14bf8ab78e2c..a3180738fa60 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -81,6 +81,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private QSExpansionPathInterpolator mQSExpansionPathInterpolator; private TouchAnimator mFirstPageAnimator; private TouchAnimator mFirstPageDelayedAnimator; + private TouchAnimator mTranslationXAnimator; private TouchAnimator mTranslationYAnimator; private TouchAnimator mNonfirstPageAnimator; private TouchAnimator mNonfirstPageDelayedAnimator; @@ -139,9 +140,11 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha updateAnimators(); } - - public void onQsScrollingChanged() { - // Lazily update animators whenever the scrolling changes + /** + * Request an update to the animators. This will update them lazily next time the position + * is changed. + */ + public void requestAnimatorUpdate() { mNeedsAnimatorUpdate = true; } @@ -223,18 +226,25 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha View qqsView, View qsView, View commonParent, + int xOffset, int yOffset, int[] temp, - TouchAnimator.Builder animatorBuilder + TouchAnimator.Builder animatorBuilderX, + TouchAnimator.Builder animatorBuilderY ) { getRelativePosition(temp, qqsView, commonParent); - int qqsPos = temp[1]; + int qqsPosX = temp[0]; + int qqsPosY = temp[1]; getRelativePosition(temp, qsView, commonParent); - int qsPos = temp[1]; - - int diff = qsPos - qqsPos - yOffset; - animatorBuilder.addFloat(qqsView, "translationY", 0, diff); - animatorBuilder.addFloat(qsView, "translationY", -diff, 0); + int qsPosX = temp[0]; + int qsPosY = temp[1]; + + int xDiff = qsPosX - qqsPosX - xOffset; + animatorBuilderX.addFloat(qqsView, "translationX", 0, xDiff); + animatorBuilderX.addFloat(qsView, "translationX", -xDiff, 0); + int yDiff = qsPosY - qqsPosY - yOffset; + animatorBuilderY.addFloat(qqsView, "translationY", 0, yDiff); + animatorBuilderY.addFloat(qsView, "translationY", -yDiff, 0); mAllViews.add(qqsView); mAllViews.add(qsView); } @@ -243,6 +253,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mNeedsAnimatorUpdate = false; TouchAnimator.Builder firstPageBuilder = new Builder(); TouchAnimator.Builder translationYBuilder = new Builder(); + TouchAnimator.Builder translationXBuilder = new Builder(); Collection<QSTile> tiles = mHost.getTiles(); int count = 0; @@ -289,6 +300,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha getRelativePosition(loc1, quickTileView, view); getRelativePosition(loc2, tileView, view); int yOffset = loc2[1] - loc1[1]; + int xOffset = loc2[0] - loc1[0]; // Offset the translation animation on the views // (that goes from 0 to getOffsetTranslation) @@ -299,6 +311,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha translationYBuilder.addFloat(tileView, "translationY", -offsetWithQSBHTranslation, 0); + translationXBuilder.addFloat(quickTileView, "translationX", 0, xOffset); + translationXBuilder.addFloat(tileView, "translationX", -xOffset, 0); + if (mQQSTileHeightAnimator == null) { mQQSTileHeightAnimator = new HeightExpansionAnimator(this, quickTileView.getHeight(), tileView.getHeight()); @@ -312,8 +327,10 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha quickTileView.getIcon(), tileView.getIcon(), view, + xOffset, yOffset, loc1, + translationXBuilder, translationYBuilder ); @@ -322,8 +339,10 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha quickTileView.getLabelContainer(), tileView.getLabelContainer(), view, + xOffset, yOffset, loc1, + translationXBuilder, translationYBuilder ); @@ -332,8 +351,10 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha quickTileView.getSecondaryIcon(), tileView.getSecondaryIcon(), view, + xOffset, yOffset, loc1, + translationXBuilder, translationYBuilder ); @@ -364,6 +385,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mOtherTilesExpandAnimator.addView(tileView); tileView.setClipChildren(true); tileView.setClipToPadding(true); + firstPageBuilder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 1); } mAllViews.add(tileView); @@ -398,10 +420,16 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha // Fade in the security footer and the divider as we reach the final position builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY); builder.addFloat(mSecurityFooter.getView(), "alpha", 0, 1); + if (mQsPanelController.shouldUseHorizontalLayout() + && mQsPanelController.mMediaHost.hostView != null) { + builder.addFloat(mQsPanelController.mMediaHost.hostView, "alpha", 0, 1); + } mAllPagesDelayedAnimator = builder.build(); mAllViews.add(mSecurityFooter.getView()); translationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()); + translationXBuilder.setInterpolator(mQSExpansionPathInterpolator.getXInterpolator()); mTranslationYAnimator = translationYBuilder.build(); + mTranslationXAnimator = translationXBuilder.build(); if (mQQSTileHeightAnimator != null) { mQQSTileHeightAnimator.setInterpolator( mQSExpansionPathInterpolator.getYInterpolator()); @@ -474,6 +502,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mFirstPageAnimator.setPosition(position); mFirstPageDelayedAnimator.setPosition(position); mTranslationYAnimator.setPosition(position); + mTranslationXAnimator.setPosition(position); if (mQQSTileHeightAnimator != null) { mQQSTileHeightAnimator.setPosition(position); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 6660081006cd..e9b19e5cfa6f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -28,12 +28,8 @@ import android.view.View; import android.view.WindowInsets; import android.widget.FrameLayout; -import androidx.dynamicanimation.animation.FloatPropertyCompat; -import androidx.dynamicanimation.animation.SpringForce; - import com.android.systemui.R; import com.android.systemui.qs.customize.QSCustomizer; -import com.android.wm.shell.animation.PhysicsAnimator; /** * Wrapper view with background which contains {@link QSPanel} and {@link QuickStatusBarHeader} @@ -41,26 +37,10 @@ import com.android.wm.shell.animation.PhysicsAnimator; public class QSContainerImpl extends FrameLayout { private final Point mSizePoint = new Point(); - private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM = - new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") { - @Override - public float getValue(QSContainerImpl qsImpl) { - return qsImpl.getBackgroundBottom(); - } - - @Override - public void setValue(QSContainerImpl background, float value) { - background.setBackgroundBottom((int) value); - } - }; - private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING - = new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, - SpringForce.DAMPING_RATIO_LOW_BOUNCY); private int mFancyClippingTop; private int mFancyClippingBottom; private final float[] mFancyClippingRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; private final Path mFancyClippingPath = new Path(); - private int mBackgroundBottom = 0; private int mHeightOverride = -1; private View mQSDetail; private QuickStatusBarHeader mHeader; @@ -71,7 +51,6 @@ public class QSContainerImpl extends FrameLayout { private int mSideMargins; private boolean mQsDisabled; private int mContentPadding = -1; - private boolean mAnimateBottomOnNextLayout; private int mNavBarInset = 0; private boolean mClippingEnabled; @@ -86,11 +65,6 @@ public class QSContainerImpl extends FrameLayout { mQSDetail = findViewById(R.id.qs_detail); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); - mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> { - if (mHeader.getHeaderQsPanel().isShown()) { - mAnimateBottomOnNextLayout = true; - } - }); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } @@ -99,20 +73,6 @@ public class QSContainerImpl extends FrameLayout { return false; } - void onMediaVisibilityChanged(boolean qsVisible) { - mAnimateBottomOnNextLayout = qsVisible; - } - - private void setBackgroundBottom(int value) { - // We're saving the bottom separately since otherwise the bottom would be overridden in - // the layout and the animation wouldn't properly start at the old position. - mBackgroundBottom = value; - } - - private float getBackgroundBottom() { - return mBackgroundBottom; - } - @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -186,8 +146,7 @@ public class QSContainerImpl extends FrameLayout { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - updateExpansion(mAnimateBottomOnNextLayout /* animate */); - mAnimateBottomOnNextLayout = false; + updateExpansion(); updateClippingPath(); } @@ -230,31 +189,12 @@ public class QSContainerImpl extends FrameLayout { } public void updateExpansion() { - updateExpansion(false /* animate */); - } - - public void updateExpansion(boolean animate) { int height = calculateContainerHeight(); int scrollBottom = calculateContainerBottom(); setBottom(getTop() + height); mQSDetail.setBottom(getTop() + scrollBottom); int qsDetailBottomMargin = ((MarginLayoutParams) mQSDetail.getLayoutParams()).bottomMargin; mQSDetail.setBottom(getTop() + scrollBottom - qsDetailBottomMargin); - updateBackgroundBottom(scrollBottom, animate); - } - - private void updateBackgroundBottom(int height, boolean animated) { - PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this); - if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) { - // An animation is running or we want to animate - // Let's make sure to set the currentValue again, since the call below might only - // start in the next frame and otherwise we'd flicker - BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this)); - physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start(); - } else { - BACKGROUND_BOTTOM.setValue(this, height); - } - } protected int calculateContainerHeight() { @@ -275,7 +215,7 @@ public class QSContainerImpl extends FrameLayout { public void setExpansion(float expansion) { mQsExpansion = expansion; - mQSPanelContainer.setScrollingEnabled(expansion > 0.0f); + mQSPanelContainer.setScrollingEnabled(expansion > 0f); updateExpansion(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java index 3638395be29e..7d61991c910a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java @@ -61,12 +61,6 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> { @Override protected void onViewAttached() { mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController); - mQsPanelController.setMediaVisibilityChangedListener((visible) -> { - if (mQsPanelController.isShown()) { - mView.onMediaVisibilityChanged(true); - } - }); - mConfigurationController.addCallback(mConfigurationListener); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index c28c649b0306..36b4ee987d99 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -53,6 +53,8 @@ import com.android.systemui.util.InjectionInflationController; import com.android.systemui.util.LifecycleFragment; import com.android.systemui.util.Utils; +import java.util.function.Consumer; + import javax.inject.Inject; import javax.inject.Named; @@ -106,6 +108,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private QSPanelController mQSPanelController; private QuickQSPanelController mQuickQSPanelController; private QSCustomizerController mQSCustomizerController; + private ScrollListener mScrollListener; private FeatureFlags mFeatureFlags; /** * When true, QS will translate from outside the screen. It will be clipped with parallax @@ -169,9 +172,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca }); mQSPanelScrollView.setOnScrollChangeListener( (v, scrollX, scrollY, oldScrollX, oldScrollY) -> { - // Lazily update animators whenever the scrolling changes - mQSAnimator.onQsScrollingChanged(); - mHeader.setExpandedScrollAmount(scrollY); + // Lazily update animators whenever the scrolling changes + mQSAnimator.requestAnimatorUpdate(); + mHeader.setExpandedScrollAmount(scrollY); + if (mScrollListener != null) { + mScrollListener.onQsPanelScrollChanged(scrollY); + } }); mQSDetail = view.findViewById(R.id.qs_detail); mHeader = view.findViewById(R.id.header); @@ -209,6 +215,19 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca setQsExpansion(mLastQSExpansion, mLastHeaderTranslation); } }); + mQSPanelController.setUsingHorizontalLayoutChangeListener( + () -> { + // The hostview may be faded out in the horizontal layout. Let's make sure to + // reset the alpha when switching layouts. This is fine since the animator will + // update the alpha if it's not supposed to be 1.0f + mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f); + mQSAnimator.requestAnimatorUpdate(); + }); + } + + @Override + public void setScrollListener(ScrollListener listener) { + mScrollListener = listener; } @Override @@ -220,6 +239,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } mQSCustomizerController.setQs(null); mQsDetailDisplayer.setQsPanelController(null); + mScrollListener = null; } @Override @@ -281,6 +301,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca return mLastQSExpansion == 0.0f || mLastQSExpansion == -1; } + @Override + public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) { + mQuickQSPanelController.setMediaVisibilityChangedListener(listener); + } + private void setEditLocation(View view) { View edit = view.findViewById(android.R.id.edit); int[] loc = edit.getLocationOnScreen(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index c70eaffcaeb6..7c7f56658919 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -46,7 +46,6 @@ import com.android.systemui.util.animation.UniqueObjectHostView; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; /** View that represents the quick settings tile panel (when expanded/pulled down). **/ public class QSPanel extends LinearLayout implements Tunable { @@ -99,13 +98,8 @@ public class QSPanel extends LinearLayout implements Tunable { private LinearLayout mHorizontalLinearLayout; protected LinearLayout mHorizontalContentContainer; - // Only used with media - private QSTileLayout mHorizontalTileLayout; - protected QSTileLayout mRegularTileLayout; protected QSTileLayout mTileLayout; - private int mLastOrientation = -1; private int mMediaTotalBottomMargin; - private Consumer<Boolean> mMediaVisibilityChangedListener; public QSPanel(Context context, AttributeSet attrs) { super(context, attrs); @@ -121,8 +115,7 @@ public class QSPanel extends LinearLayout implements Tunable { } void initialize() { - mRegularTileLayout = createRegularTileLayout(); - mTileLayout = mRegularTileLayout; + mTileLayout = getOrCreateTileLayout(); if (mUsingMediaPlayer) { mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); @@ -135,7 +128,6 @@ public class QSPanel extends LinearLayout implements Tunable { mHorizontalContentContainer.setClipChildren(true); mHorizontalContentContainer.setClipToPadding(false); - mHorizontalTileLayout = createHorizontalTileLayout(); LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); int marginSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_padding); lp.setMarginStart(0); @@ -148,12 +140,6 @@ public class QSPanel extends LinearLayout implements Tunable { } } - protected void onMediaVisibilityChanged(Boolean visible) { - if (mMediaVisibilityChangedListener != null) { - mMediaVisibilityChangedListener.accept(visible); - } - } - /** * Add brightness view above the tile layout. * @@ -184,17 +170,12 @@ public class QSPanel extends LinearLayout implements Tunable { } /** */ - public QSTileLayout createRegularTileLayout() { - if (mRegularTileLayout == null) { - mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext) + public QSTileLayout getOrCreateTileLayout() { + if (mTileLayout == null) { + mTileLayout = (QSTileLayout) LayoutInflater.from(mContext) .inflate(R.layout.qs_paged_tile_layout, this, false); } - return mRegularTileLayout; - } - - - protected QSTileLayout createHorizontalTileLayout() { - return createRegularTileLayout(); + return mTileLayout; } @Override @@ -281,18 +262,18 @@ public class QSPanel extends LinearLayout implements Tunable { * @param pageIndicator indicator to use for page scrolling */ public void setFooterPageIndicator(PageIndicator pageIndicator) { - if (mRegularTileLayout instanceof PagedTileLayout) { + if (mTileLayout instanceof PagedTileLayout) { mFooterPageIndicator = pageIndicator; updatePageIndicator(); } } private void updatePageIndicator() { - if (mRegularTileLayout instanceof PagedTileLayout) { + if (mTileLayout instanceof PagedTileLayout) { if (mFooterPageIndicator != null) { mFooterPageIndicator.setVisibility(View.GONE); - ((PagedTileLayout) mRegularTileLayout).setPageIndicator(mFooterPageIndicator); + ((PagedTileLayout) mTileLayout).setPageIndicator(mFooterPageIndicator); } } } @@ -362,7 +343,7 @@ public class QSPanel extends LinearLayout implements Tunable { return true; } - protected boolean needsDynamicRowsAndColumns() { + private boolean needsDynamicRowsAndColumns() { return true; } @@ -667,10 +648,6 @@ public class QSPanel extends LinearLayout implements Tunable { mHeaderContainer = headerContainer; } - public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { - mMediaVisibilityChangedListener = visibilityChangedListener; - } - public boolean isListening() { return mListening; } @@ -681,39 +658,20 @@ public class QSPanel extends LinearLayout implements Tunable { } protected void setPageMargin(int pageMargin) { - if (mRegularTileLayout instanceof PagedTileLayout) { - ((PagedTileLayout) mRegularTileLayout).setPageMargin(pageMargin); - } - if (mHorizontalTileLayout != mRegularTileLayout - && mHorizontalTileLayout instanceof PagedTileLayout) { - ((PagedTileLayout) mHorizontalTileLayout).setPageMargin(pageMargin); + if (mTileLayout instanceof PagedTileLayout) { + ((PagedTileLayout) mTileLayout).setPageMargin(pageMargin); } } - void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force, - UiEventLogger uiEventLogger) { + void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force) { if (horizontal != mUsingHorizontalLayout || force) { mUsingHorizontalLayout = horizontal; - View visibleView = horizontal ? mHorizontalLinearLayout : (View) mRegularTileLayout; - View hiddenView = horizontal ? (View) mRegularTileLayout : mHorizontalLinearLayout; ViewGroup newParent = horizontal ? mHorizontalContentContainer : this; - QSPanel.QSTileLayout newLayout = horizontal - ? mHorizontalTileLayout : mRegularTileLayout; - if (hiddenView != null - && (mRegularTileLayout != mHorizontalTileLayout - || hiddenView != mRegularTileLayout)) { - // Only hide the view if the horizontal and the regular view are different, - // otherwise its reattached. - hiddenView.setVisibility(View.GONE); - } - visibleView.setVisibility(View.VISIBLE); - switchAllContentToParent(newParent, newLayout); + switchAllContentToParent(newParent, mTileLayout); reAttachMediaHost(mediaHostView, horizontal); - mTileLayout = newLayout; - newLayout.setListening(mListening, uiEventLogger); if (needsDynamicRowsAndColumns()) { - newLayout.setMinRows(horizontal ? 2 : 1); - newLayout.setMaxColumns(horizontal ? 2 : 4); + mTileLayout.setMinRows(horizontal ? 2 : 1); + mTileLayout.setMaxColumns(horizontal ? 2 : 4); } updateMargins(mediaHostView); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index ac92d4fe44e2..ae0f5104d20f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -45,8 +45,6 @@ import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.tuner.TunerService; -import java.util.function.Consumer; - import javax.inject.Inject; import javax.inject.Named; @@ -149,14 +147,14 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mBrightnessMirrorController.addCallback(mBrightnessMirrorListener); } - ((PagedTileLayout) mView.createRegularTileLayout()) + ((PagedTileLayout) mView.getOrCreateTileLayout()) .setOnTouchListener(mTileLayoutTouchListener); } @Override protected QSTileRevealController createTileRevealController() { return mQsTileRevealControllerFactory.create( - this, (PagedTileLayout) mView.createRegularTileLayout()); + this, (PagedTileLayout) mView.getOrCreateTileLayout()); } @Override @@ -289,11 +287,6 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mView.setPageListener(listener); } - /** */ - public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { - mView.setMediaVisibilityChangedListener(visibilityChangedListener); - } - public boolean isShown() { return mView.isShown(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 170785ca7aab..4739a3f4c7d6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -19,6 +19,8 @@ package com.android.systemui.qs; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.res.Configuration; import android.metrics.LogMaker; @@ -42,6 +44,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.inject.Named; @@ -68,6 +71,8 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); private boolean mShouldUseSplitNotificationShade; + @Nullable + private Consumer<Boolean> mMediaVisibilityChangedListener; private int mLastOrientation; private String mCachedSpecs = ""; private QSTileRevealController mQsTileRevealController; @@ -89,13 +94,18 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr }; private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> { - mView.onMediaVisibilityChanged(visible); + if (mMediaVisibilityChangedListener != null) { + mMediaVisibilityChangedListener.accept(visible); + } switchTileLayout(false); return null; }; private boolean mUsingHorizontalLayout; + @Nullable + private Runnable mUsingHorizontalLayoutChangedListener; + protected QSPanelControllerBase( T view, QSTileHost host, @@ -128,6 +138,13 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); } + /** + * @return the media host for this panel + */ + public MediaHost getMediaHost() { + return mMediaHost; + } + @Override protected void onViewAttached() { mQsTileRevealController = createTileRevealController(); @@ -136,7 +153,6 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); - mView.onMediaVisibilityChanged(mMediaHost.getVisible()); mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); mHost.addCallback(mQSHostCallback); setTiles(); @@ -291,20 +307,15 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } boolean switchTileLayout(boolean force) { - /** Whether or not the QuickQSPanel currently contains a media player. */ + /* Whether or not the panel currently contains a media player. */ boolean horizontal = shouldUseHorizontalLayout(); if (horizontal != mUsingHorizontalLayout || force) { mUsingHorizontalLayout = horizontal; - for (QSPanelControllerBase.TileRecord record : mRecords) { - mView.removeTile(record); - record.tile.removeCallback(record.callback); - } - mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force, - mUiEventLogger); + mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force); updateMediaDisappearParameters(); - - setTiles(); - + if (mUsingHorizontalLayoutChangedListener != null) { + mUsingHorizontalLayoutChangedListener.run(); + } return true; } return false; @@ -381,6 +392,20 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr return mView.getTileLayout(); } + /** + * Add a listener for when the media visibility changes. + */ + public void setMediaVisibilityChangedListener(@NonNull Consumer<Boolean> listener) { + mMediaVisibilityChangedListener = listener; + } + + /** + * Add a listener when the horizontal layout changes + */ + public void setUsingHorizontalLayoutChangeListener(Runnable listener) { + mUsingHorizontalLayoutChangedListener = listener; + } + /** */ public static final class TileRecord extends QSPanel.Record { public QSTile tile; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 3a6f1d5a02ae..7f19d0e6c25c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -185,15 +185,22 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen final boolean isProfileOwnerOfOrganizationOwnedDevice = mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled(); + final boolean isWorkProfileOn = mSecurityController.isWorkProfileOn(); + final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile + || vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled); // Update visibility of footer - mIsVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || hasCACertsInWorkProfile - || vpnName != null || vpnNameWorkProfile != null - || isProfileOwnerOfOrganizationOwnedDevice || isParentalControlsEnabled - || (hasWorkProfile && isNetworkLoggingEnabled); + mIsVisible = (isDeviceManaged && !isDemoDevice) + || hasCACerts + || vpnName != null + || isProfileOwnerOfOrganizationOwnedDevice + || isParentalControlsEnabled + || (hasDisclosableWorkProfilePolicy && isWorkProfileOn); // Update the view to be untappable if the device is an organization-owned device with a - // managed profile and there is no policy set which requires a privacy disclosure. - if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice && !isNetworkLoggingEnabled - && !hasCACertsInWorkProfile && vpnNameWorkProfile == null) { + // managed profile and there is either: + // a) no policy set which requires a privacy disclosure. + // b) a specific work policy set but the work profile is turned off. + if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice + && (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)) { mRootView.setClickable(false); mRootView.findViewById(R.id.footer_icon).setVisibility(View.GONE); } else { @@ -204,7 +211,8 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName, workProfileOrganizationName, - isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled); + isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled, + isWorkProfileOn); // Update the icon int footerIconId = R.drawable.ic_info_outline; if (vpnName != null || vpnNameWorkProfile != null) { @@ -236,7 +244,8 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, String vpnName, String vpnNameWorkProfile, CharSequence organizationName, CharSequence workProfileOrganizationName, - boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled) { + boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled, + boolean isWorkProfileOn) { if (isParentalControlsEnabled) { return mContext.getString(R.string.quick_settings_disclosure_parental_controls); } @@ -280,7 +289,7 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen organizationName); } } // end if(isDeviceManaged) - if (hasCACertsInWorkProfile) { + if (hasCACertsInWorkProfile && isWorkProfileOn) { if (workProfileOrganizationName == null) { return mContext.getString( R.string.quick_settings_disclosure_managed_profile_monitoring); @@ -295,7 +304,7 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen if (vpnName != null && vpnNameWorkProfile != null) { return mContext.getString(R.string.quick_settings_disclosure_vpns); } - if (vpnNameWorkProfile != null) { + if (vpnNameWorkProfile != null && isWorkProfileOn) { return mContext.getString(R.string.quick_settings_disclosure_managed_profile_named_vpn, vpnNameWorkProfile); } @@ -308,7 +317,7 @@ class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListen return mContext.getString(R.string.quick_settings_disclosure_named_vpn, vpnName); } - if (hasWorkProfile && isNetworkLoggingEnabled) { + if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) { return mContext.getString( R.string.quick_settings_disclosure_managed_profile_network_activity); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 6ddf2a75f491..756ad9939886 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -27,7 +27,6 @@ import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.ArraySet; -import android.util.FeatureFlagUtils; import android.util.Log; import com.android.internal.logging.InstanceId; @@ -52,6 +51,7 @@ import com.android.systemui.qs.external.TileServices; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -123,7 +123,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, - CustomTileStatePersister customTileStatePersister) { + CustomTileStatePersister customTileStatePersister + ) { mIconController = iconController; mContext = context; mUserContext = context; @@ -517,7 +518,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D // --WiFiTile // --CellularTIle if (tiles.contains("internet") || tiles.contains("wifi") || tiles.contains("cell")) { - if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { + if (FeatureFlags.isProviderModelSettingEnabled(context)) { if (!tiles.contains("internet")) { if (tiles.contains("wifi")) { // Replace the WiFi with Internet, and remove the Cell @@ -559,7 +560,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D } // TODO(b/174753536): Change the config file directly. // Filter out unused tiles from the default QS config. - if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { + if (FeatureFlags.isProviderModelSettingEnabled(context)) { tiles.remove("cell"); tiles.remove("wifi"); } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 659475d19277..4cd4048f7286 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -61,21 +61,10 @@ public class QuickQSPanel extends QSPanel { } @Override - public TileLayout createRegularTileLayout() { + public TileLayout getOrCreateTileLayout() { return new QQSSideLabelTileLayout(mContext); } - @Override - protected QSTileLayout createHorizontalTileLayout() { - TileLayout t = createRegularTileLayout(); - t.setMaxColumns(2); - return t; - } - - @Override - protected boolean needsDynamicRowsAndColumns() { - return false; // QQS always have the same layout - } @Override protected boolean displayMediaMarginsOnMedia() { @@ -191,6 +180,7 @@ public class QuickQSPanel extends QSPanel { LayoutParams.WRAP_CONTENT); setLayoutParams(lp); setMaxColumns(4); + mLastRowPadding = true; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 997b96626747..03a2c843a15e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -23,7 +23,6 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.FeatureFlagUtils; import android.util.Pair; import android.view.DisplayCutout; import android.view.View; @@ -79,7 +78,6 @@ public class QuickStatusBarHeader extends FrameLayout { private TintedIconManager mTintedIconManager; private QSExpansionPathInterpolator mQSExpansionPathInterpolator; - private int mStatusBarPaddingTop = 0; private int mRoundedCornerPadding = 0; private int mWaterfallTopInset; private int mCutOutPaddingLeft; @@ -88,11 +86,11 @@ public class QuickStatusBarHeader extends FrameLayout { private float mKeyguardExpansionFraction; private int mTextColorPrimary = Color.TRANSPARENT; private int mTopViewMeasureHeight; + private boolean mProviderModel; private final String mMobileSlotName; private final String mNoCallingSlotName; private final String mCallStrengthSlotName; - private final boolean mProviderModel; public QuickStatusBarHeader(Context context, AttributeSet attrs) { super(context, attrs); @@ -100,11 +98,6 @@ public class QuickStatusBarHeader extends FrameLayout { mNoCallingSlotName = context.getString(com.android.internal.R.string.status_bar_no_calling); mCallStrengthSlotName = context.getString(com.android.internal.R.string.status_bar_call_strength); - if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { - mProviderModel = true; - } else { - mProviderModel = false; - } } /** @@ -154,7 +147,9 @@ public class QuickStatusBarHeader extends FrameLayout { } void onAttach(TintedIconManager iconManager, - QSExpansionPathInterpolator qsExpansionPathInterpolator) { + QSExpansionPathInterpolator qsExpansionPathInterpolator, + boolean providerModel) { + mProviderModel = providerModel; mTintedIconManager = iconManager; int fillColor = Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorPrimary); @@ -209,7 +204,6 @@ public class QuickStatusBarHeader extends FrameLayout { mRoundedCornerPadding = resources.getDimensionPixelSize( R.dimen.rounded_corner_content_padding); - mStatusBarPaddingTop = resources.getDimensionPixelSize(R.dimen.status_bar_padding_top); int qsOffsetHeight = resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_offset_height); @@ -469,11 +463,11 @@ public class QuickStatusBarHeader extends FrameLayout { } mDatePrivacyView.setPadding(paddingLeft, - mWaterfallTopInset + mStatusBarPaddingTop, + mWaterfallTopInset, paddingRight, 0); mClockIconsView.setPadding(paddingLeft, - mWaterfallTopInset + mStatusBarPaddingTop, + mWaterfallTopInset, paddingRight, 0); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java index 76076f6c2761..fcf1302b8fb4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java @@ -37,6 +37,7 @@ import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.privacy.logging.PrivacyLogger; import com.android.systemui.qs.carrier.QSCarrierGroupController; import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.policy.Clock; @@ -69,6 +70,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader private final PrivacyLogger mPrivacyLogger; private final PrivacyDialogController mPrivacyDialogController; private final QSExpansionPathInterpolator mQSExpansionPathInterpolator; + private final FeatureFlags mFeatureFlags; private boolean mListening; private boolean mMicCameraIndicatorsEnabled; @@ -130,7 +132,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader PrivacyLogger privacyLogger, SysuiColorExtractor colorExtractor, PrivacyDialogController privacyDialogController, - QSExpansionPathInterpolator qsExpansionPathInterpolator) { + QSExpansionPathInterpolator qsExpansionPathInterpolator, + FeatureFlags featureFlags) { super(view); mPrivacyItemController = privacyItemController; mActivityStarter = activityStarter; @@ -141,6 +144,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mPrivacyLogger = privacyLogger; mPrivacyDialogController = privacyDialogController; mQSExpansionPathInterpolator = qsExpansionPathInterpolator; + mFeatureFlags = featureFlags; mQSCarrierGroupController = qsCarrierGroupControllerBuilder .setQSCarrierGroup(mView.findViewById(R.id.carrier_group)) @@ -150,7 +154,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mClockView = mView.findViewById(R.id.clock); mIconContainer = mView.findViewById(R.id.statusIcons); - mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer); + mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, mFeatureFlags); mDemoModeReceiver = new ClockDemoModeReceiver(mClockView); mColorExtractor = colorExtractor; mOnColorsChangedListener = (extractor, which) -> { @@ -174,7 +178,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); - mView.onAttach(mIconManager, mQSExpansionPathInterpolator); + mView.onAttach(mIconManager, mQSExpansionPathInterpolator, + mFeatureFlags.isCombinedStatusBarSignalIconsEnabled()); mDemoModeController.addCallback(mDemoModeReceiver); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 3e558a962427..2b96a34967f4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -31,6 +31,7 @@ public class TileLayout extends ViewGroup implements QSTileLayout { protected int mCellMarginVertical; protected int mSidePadding; protected int mRows = 1; + protected boolean mLastRowPadding = false; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); protected boolean mListening; @@ -167,6 +168,10 @@ public class TileLayout extends ViewGroup implements QSTileLayout { } int height = (mCellHeight + mCellMarginVertical) * mRows; + if (!mLastRowPadding) { + height -= mCellMarginVertical; + } + if (height < 0) height = 0; setMeasuredDimension(width, height); diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/CellSignalState.kt b/packages/SystemUI/src/com/android/systemui/qs/carrier/CellSignalState.kt index 663f3f0e9ddb..2dac63905524 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/carrier/CellSignalState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/CellSignalState.kt @@ -26,7 +26,8 @@ data class CellSignalState( @JvmField val mobileSignalIconId: Int = 0, @JvmField val contentDescription: String? = null, @JvmField val typeContentDescription: String? = null, - @JvmField val roaming: Boolean = false + @JvmField val roaming: Boolean = false, + @JvmField val providerModelBehavior: Boolean = false ) { /** * Changes the visibility of this state by returning a copy with the visibility changed. diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java index ae0b5d11db13..d6fa21646402 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.FeatureFlagUtils; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; @@ -39,7 +38,7 @@ public class QSCarrier extends LinearLayout { private ImageView mMobileSignal; private ImageView mMobileRoaming; private CellSignalState mLastSignalState; - private boolean mProviderModel; + private boolean mProviderModelInitialized = false; public QSCarrier(Context context) { super(context); @@ -60,20 +59,10 @@ public class QSCarrier extends LinearLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { - mProviderModel = true; - } else { - mProviderModel = false; - } mMobileGroup = findViewById(R.id.mobile_combo); mMobileRoaming = findViewById(R.id.mobile_roaming); mMobileSignal = findViewById(R.id.mobile_signal); mCarrierText = findViewById(R.id.qs_carrier_text); - if (mProviderModel) { - mMobileSignal.setImageDrawable(mContext.getDrawable(R.drawable.ic_qs_no_calling_sms)); - } else { - mMobileSignal.setImageDrawable(new SignalDrawable(mContext)); - } } /** @@ -92,10 +81,19 @@ public class QSCarrier extends LinearLayout { mMobileRoaming.setImageTintList(colorStateList); mMobileSignal.setImageTintList(colorStateList); - if (mProviderModel) { + if (state.providerModelBehavior) { + if (!mProviderModelInitialized) { + mProviderModelInitialized = true; + mMobileSignal.setImageDrawable( + mContext.getDrawable(R.drawable.ic_qs_no_calling_sms)); + } mMobileSignal.setImageDrawable(mContext.getDrawable(state.mobileSignalIconId)); mMobileSignal.setContentDescription(state.contentDescription); } else { + if (!mProviderModelInitialized) { + mProviderModelInitialized = true; + mMobileSignal.setImageDrawable(new SignalDrawable(mContext)); + } mMobileSignal.setImageLevel(state.mobileSignalIconId); StringBuilder contentDescription = new StringBuilder(); if (state.contentDescription != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java index c49e0547e433..f23c0580c409 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java @@ -27,7 +27,6 @@ import android.os.Message; import android.provider.Settings; import android.telephony.SubscriptionManager; import android.text.TextUtils; -import android.util.FeatureFlagUtils; import android.util.Log; import android.view.View; import android.widget.TextView; @@ -41,6 +40,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators; import com.android.systemui.util.CarrierConfigTracker; @@ -95,7 +95,8 @@ public class QSCarrierGroupController { indicators.statusIcon.icon, indicators.statusIcon.contentDescription, indicators.typeContentDescription.toString(), - indicators.roaming + indicators.roaming, + mProviderModel ); mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); } @@ -120,18 +121,32 @@ public class QSCarrierGroupController { if (statusIcon.icon == R.drawable.ic_qs_no_calling_sms) { if (statusIcon.visible) { - mInfos[slotIndex] = new CellSignalState(true, - statusIcon.icon, statusIcon.contentDescription, "", false); + mInfos[slotIndex] = new CellSignalState( + true, + statusIcon.icon, + statusIcon.contentDescription, + "", + false, + mProviderModel); } else { // Whenever the no Calling & SMS state is cleared, switched to the last // known call strength icon. if (displayCallStrengthIcon) { mInfos[slotIndex] = new CellSignalState( - true, mLastSignalLevel[slotIndex], - mLastSignalLevelDescription[slotIndex], "", false); + true, + mLastSignalLevel[slotIndex], + mLastSignalLevelDescription[slotIndex], + "", + false, + mProviderModel); } else { mInfos[slotIndex] = new CellSignalState( - true, R.drawable.ic_qs_sim_card, "", "", false); + true, + R.drawable.ic_qs_sim_card, + "", + "", + false, + mProviderModel); } } mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); @@ -143,11 +158,21 @@ public class QSCarrierGroupController { if (mInfos[slotIndex].mobileSignalIconId != R.drawable.ic_qs_no_calling_sms) { if (displayCallStrengthIcon) { - mInfos[slotIndex] = new CellSignalState(true, statusIcon.icon, - statusIcon.contentDescription, "", false); + mInfos[slotIndex] = new CellSignalState( + true, + statusIcon.icon, + statusIcon.contentDescription, + "", + false, + mProviderModel); } else { mInfos[slotIndex] = new CellSignalState( - true, R.drawable.ic_qs_sim_card, "", "", false); + true, + R.drawable.ic_qs_sim_card, + "", + "", + false, + mProviderModel); } mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); } @@ -182,8 +207,9 @@ public class QSCarrierGroupController { @Background Handler bgHandler, @Main Looper mainLooper, NetworkController networkController, CarrierTextManager.Builder carrierTextManagerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker) { - if (FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { + CarrierConfigTracker carrierConfigTracker, FeatureFlags featureFlags) { + + if (featureFlags.isCombinedStatusBarSignalIconsEnabled()) { mProviderModel = true; } else { mProviderModel = false; @@ -217,9 +243,13 @@ public class QSCarrierGroupController { mCarrierDividers[1] = view.getCarrierDivider2(); for (int i = 0; i < SIM_SLOTS; i++) { - mInfos[i] = new CellSignalState(true, R.drawable.ic_qs_no_calling_sms, + mInfos[i] = new CellSignalState( + true, + R.drawable.ic_qs_no_calling_sms, context.getText(AccessibilityContentDescriptions.NO_CALLING).toString(), - "", false); + "", + false, + mProviderModel); mLastSignalLevel[i] = TelephonyIcons.MOBILE_CALL_STRENGTH_ICONS[0]; mLastSignalLevelDescription[i] = context.getText(AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0]) @@ -289,7 +319,8 @@ public class QSCarrierGroupController { for (int i = 0; i < SIM_SLOTS; i++) { if (mInfos[i].visible && mInfos[i].mobileSignalIconId == R.drawable.ic_qs_sim_card) { - mInfos[i] = new CellSignalState(true, R.drawable.ic_blank, "", "", false); + mInfos[i] = new CellSignalState(true, R.drawable.ic_blank, "", "", false, + mProviderModel); } } } @@ -401,12 +432,13 @@ public class QSCarrierGroupController { private final CarrierTextManager.Builder mCarrierTextControllerBuilder; private final Context mContext; private final CarrierConfigTracker mCarrierConfigTracker; + private final FeatureFlags mFeatureFlags; @Inject public Builder(ActivityStarter activityStarter, @Background Handler handler, @Main Looper looper, NetworkController networkController, CarrierTextManager.Builder carrierTextControllerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker) { + CarrierConfigTracker carrierConfigTracker, FeatureFlags featureFlags) { mActivityStarter = activityStarter; mHandler = handler; mLooper = looper; @@ -414,6 +446,7 @@ public class QSCarrierGroupController { mCarrierTextControllerBuilder = carrierTextControllerBuilder; mContext = context; mCarrierConfigTracker = carrierConfigTracker; + mFeatureFlags = featureFlags; } public Builder setQSCarrierGroup(QSCarrierGroup view) { @@ -424,7 +457,7 @@ public class QSCarrierGroupController { public QSCarrierGroupController build() { return new QSCarrierGroupController(mView, mActivityStarter, mHandler, mLooper, mNetworkController, mCarrierTextControllerBuilder, mContext, - mCarrierConfigTracker); + mCarrierConfigTracker, mFeatureFlags); } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index d017c74b4306..b904505b6469 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -567,6 +567,8 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta public void clearDrag() { itemView.clearAnimation(); + itemView.setScaleX(1); + itemView.setScaleY(1); } public void startDrag() { @@ -812,5 +814,12 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta @Override public void onSwiped(ViewHolder viewHolder, int direction) { } + + // Just in case, make sure to animate to base state. + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { + ((Holder) viewHolder).stopDrag(); + super.clearView(recyclerView, viewHolder); + } }; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index d72f8e9ca1c0..3cb715cee8e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -29,7 +29,6 @@ import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArraySet; -import android.util.FeatureFlagUtils; import android.widget.Button; import com.android.systemui.R; @@ -42,6 +41,7 @@ import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; @@ -63,17 +63,24 @@ public class TileQueryHelper { private final Executor mBgExecutor; private final Context mContext; private final UserTracker mUserTracker; + private final FeatureFlags mFeatureFlags; private TileStateListener mListener; private boolean mFinished; @Inject - public TileQueryHelper(Context context, UserTracker userTracker, - @Main Executor mainExecutor, @Background Executor bgExecutor) { + public TileQueryHelper( + Context context, + UserTracker userTracker, + @Main Executor mainExecutor, + @Background Executor bgExecutor, + FeatureFlags featureFlags + ) { mContext = context; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mUserTracker = userTracker; + mFeatureFlags = featureFlags; } public void setListener(TileStateListener listener) { @@ -115,7 +122,7 @@ public class TileQueryHelper { final ArrayList<QSTile> tilesToAdd = new ArrayList<>(); // TODO(b/174753536): Move it into the config file. - if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { + if (mFeatureFlags.isProviderModelSettingEnabled()) { possibleTiles.remove("cell"); possibleTiles.remove("wifi"); } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index aa51771864b2..1d791f5d632c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -57,6 +57,12 @@ public class TileLifecycleManager extends BroadcastReceiver implements private static final String TAG = "TileLifecycleManager"; + private static final int META_DATA_QUERY_FLAGS = + PackageManager.GET_META_DATA + | PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE; + private static final int MSG_ON_ADDED = 0; private static final int MSG_ON_REMOVED = 1; private static final int MSG_ON_CLICK = 2; @@ -130,7 +136,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements public boolean isActiveTile() { try { ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(), - PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA); + META_DATA_QUERY_FLAGS); return info.metaData != null && info.metaData.getBoolean(TileService.META_DATA_ACTIVE_TILE, false); } catch (PackageManager.NameNotFoundException e) { @@ -148,7 +154,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements public boolean isToggleableTile() { try { ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(), - PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA); + META_DATA_QUERY_FLAGS); return info.metaData != null && info.metaData.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false); } catch (PackageManager.NameNotFoundException e) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/IgnorableChildLinearLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/IgnorableChildLinearLayout.kt index 2bac29846893..56bf3d59f648 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/IgnorableChildLinearLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/IgnorableChildLinearLayout.kt @@ -23,7 +23,7 @@ import android.widget.LinearLayout /** * [LinearLayout] that can ignore the last child for measuring. * - * The view is measured as regularlt, then if [ignoreLastView] is true: + * The view is measured as regularly, then if [ignoreLastView] is true: * * In [LinearLayout.VERTICAL] orientation, the height of the last view is subtracted from the * final measured height. * * In [LinearLayout.HORIZONTAL] orientation, the width of the last view is subtracted from the @@ -41,8 +41,21 @@ class IgnorableChildLinearLayout @JvmOverloads constructor( var ignoreLastView = false + /** + * Forces [MeasureSpec.UNSPECIFIED] in the direction of layout + */ + var forceUnspecifiedMeasure = false + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val actualWidthSpec = if (forceUnspecifiedMeasure && orientation == HORIZONTAL) { + MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.UNSPECIFIED) + } else widthMeasureSpec + + val actualHeightSpec = if (forceUnspecifiedMeasure && orientation == VERTICAL) { + MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.UNSPECIFIED) + } else heightMeasureSpec + + super.onMeasure(actualWidthSpec, actualHeightSpec) if (ignoreLastView && childCount > 0) { val lastView = getChildAt(childCount - 1) if (lastView.visibility != GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index d31e67c55777..70685a68e182 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -189,6 +189,11 @@ open class QSTileViewImpl @JvmOverloads constructor( secondaryLabel = labelContainer.requireViewById(R.id.app_label) if (collapsed) { labelContainer.ignoreLastView = true + // Ideally, it'd be great if the parent could set this up when measuring just this child + // instead of the View class having to support this. However, due to the mysteries of + // LinearLayout's double measure pass, we cannot overwrite `measureChild` or any of its + // sibling methods to have special behavior for labelContainer. + labelContainer.forceUnspecifiedMeasure = true secondaryLabel.alpha = 0f // Do not marque in QQS label.ellipsize = TextUtils.TruncateAt.END diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java index 98cd88af232f..82b6c0c1805d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java @@ -73,6 +73,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { private final QuickAccessWalletController mController; private WalletCard mSelectedCard; + private boolean mIsWalletUpdating = true; @VisibleForTesting Drawable mCardViewDrawable; @Inject @@ -110,7 +111,8 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { super.handleSetListening(listening); if (listening) { mController.setupWalletChangeObservers(mCardRetriever, DEFAULT_PAYMENT_APP_CHANGE); - if (!mController.getWalletClient().isWalletServiceAvailable()) { + if (!mController.getWalletClient().isWalletServiceAvailable() + || !mController.getWalletClient().isWalletFeatureAvailable()) { Log.i(TAG, "QAW service is unavailable, recreating the wallet client."); mController.reCreateWalletClient(); } @@ -156,9 +158,14 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { CharSequence label = mController.getWalletClient().getServiceLabel(); state.label = label == null ? mLabel : label; state.contentDescription = state.label; - state.icon = ResourceIcon.get(R.drawable.ic_wallet_lockscreen); + Drawable tileIcon = mController.getWalletClient().getTileIcon(); + state.icon = + tileIcon == null + ? ResourceIcon.get(R.drawable.ic_wallet_lockscreen) + : new DrawableIcon(tileIcon); boolean isDeviceLocked = !mKeyguardStateController.isUnlocked(); - if (mController.getWalletClient().isWalletServiceAvailable()) { + if (mController.getWalletClient().isWalletServiceAvailable() + && mController.getWalletClient().isWalletFeatureAvailable()) { if (mSelectedCard != null) { if (isDeviceLocked) { state.state = Tile.STATE_INACTIVE; @@ -172,7 +179,11 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { } } else { state.state = Tile.STATE_INACTIVE; - state.secondaryLabel = mContext.getString(R.string.wallet_secondary_label_no_card); + state.secondaryLabel = + mContext.getString( + mIsWalletUpdating + ? R.string.wallet_secondary_label_updating + : R.string.wallet_secondary_label_no_card); state.sideViewCustomDrawable = null; } state.stateDescription = state.secondaryLabel; @@ -218,6 +229,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { @Override public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) { Log.i(TAG, "Successfully retrieved wallet cards."); + mIsWalletUpdating = false; List<WalletCard> cards = response.getWalletCards(); if (cards.isEmpty()) { Log.d(TAG, "No wallet cards exist."); @@ -240,7 +252,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { @Override public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { - Log.w(TAG, "Error retrieve wallet cards"); + mIsWalletUpdating = false; mCardViewDrawable = null; mSelectedCard = null; refreshState(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java index f13576c2d4cc..bf72b7728232 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java @@ -16,6 +16,8 @@ package com.android.systemui.qs.tiles; +import static android.hardware.SensorPrivacyManager.Sources.QS_TILE; + import android.content.Intent; import android.hardware.SensorPrivacyManager.Sensors.Sensor; import android.os.Handler; @@ -87,12 +89,12 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS protected void handleClick(@Nullable View view) { if (mKeyguard.isMethodSecure() && mKeyguard.isShowing()) { mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - mSensorPrivacyController.setSensorBlocked(getSensorId(), + mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(), !mSensorPrivacyController.isSensorBlocked(getSensorId())); }); return; } - mSensorPrivacyController.setSensorBlocked(getSensorId(), + mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(), !mSensorPrivacyController.isSensorBlocked(getSensorId())); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index c6b5eb7508af..5bb3413595ba 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -291,19 +291,24 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis ? res.getString(R.string.screenrecord_ongoing_screen_only) : res.getString(R.string.screenrecord_ongoing_screen_and_audio); - Intent stopIntent = getNotificationIntent(this); + PendingIntent pendingIntent = PendingIntent.getService( + this, + REQUEST_CODE, + getNotificationIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + Notification.Action stopAction = new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), + getResources().getString(R.string.screenrecord_stop_label), + pendingIntent).build(); Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_screenrecord) .setContentTitle(notificationTitle) - .setContentText(getResources().getString(R.string.screenrecord_stop_text)) .setUsesChronometer(true) .setColorized(true) .setColor(getResources().getColor(R.color.GM2_red_700)) .setOngoing(true) .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) - .setContentIntent( - PendingIntent.getService(this, REQUEST_CODE, stopIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .addAction(stopAction) .addExtras(extras); startForeground(NOTIFICATION_RECORDING_ID, builder.build()); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 57125f34731c..df766f3625e4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -30,9 +30,9 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.Spinner; import android.widget.Switch; +import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.settings.UserContextProvider; @@ -78,12 +78,12 @@ public class ScreenRecordDialog extends Activity { setContentView(R.layout.screen_record_dialog); - Button cancelBtn = findViewById(R.id.button_cancel); + TextView cancelBtn = findViewById(R.id.button_cancel); cancelBtn.setOnClickListener(v -> { finish(); }); - Button startBtn = findViewById(R.id.button_start); + TextView startBtn = findViewById(R.id.button_start); startBtn.setOnClickListener(v -> { requestScreenCapture(); finish(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java index 0a60f6da159e..a9cecaaf1f76 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java @@ -44,6 +44,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.customview.widget.ExploreByTouchHelper; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import com.android.internal.graphics.ColorUtils; import com.android.systemui.R; import java.util.List; @@ -95,7 +96,9 @@ public class CropView extends View { TypedArray t = context.getTheme().obtainStyledAttributes( attrs, R.styleable.CropView, 0, 0); mShadePaint = new Paint(); - mShadePaint.setColor(t.getColor(R.styleable.CropView_scrimColor, Color.TRANSPARENT)); + int alpha = t.getInteger(R.styleable.CropView_scrimAlpha, 255); + int scrimColor = t.getColor(R.styleable.CropView_scrimColor, Color.TRANSPARENT); + mShadePaint.setColor(ColorUtils.setAlphaComponent(scrimColor, alpha)); mContainerBackgroundPaint = new Paint(); mContainerBackgroundPaint.setColor(t.getColor(R.styleable.CropView_containerBackgroundColor, Color.TRANSPARENT)); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 4aead817fe8c..55602a98b8c5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -111,30 +111,21 @@ class ImageExporter { } /** - * Stores the given Bitmap to a temp file. + * Writes the given Bitmap to outputFile. */ - ListenableFuture<File> exportAsTempFile(Executor executor, Bitmap bitmap) { + ListenableFuture<File> exportToRawFile(Executor executor, Bitmap bitmap, + final File outputFile) { return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { - File cachePath; - try { - cachePath = File.createTempFile("long_screenshot_cache_", ".tmp"); - try (FileOutputStream stream = new FileOutputStream(cachePath)) { - bitmap.compress(mCompressFormat, mQuality, stream); - } catch (IOException e) { - if (cachePath.exists()) { - //noinspection ResultOfMethodCallIgnored - cachePath.delete(); - cachePath = null; - } - completer.setException(e); - } - if (cachePath != null) { - completer.set(cachePath); - } + try (FileOutputStream stream = new FileOutputStream(outputFile)) { + bitmap.compress(mCompressFormat, mQuality, stream); + completer.set(outputFile); } catch (IOException e) { - // Failed to create a new file + if (outputFile.exists()) { + //noinspection ResultOfMethodCallIgnored + outputFile.delete(); + } completer.setException(e); } }); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index f571e417c636..af0141c81d58 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -80,6 +80,7 @@ public class LongScreenshotActivity extends Activity { private View mSave; private View mEdit; private View mShare; + private View mDelete; private CropView mCropView; private MagnifierView mMagnifierView; private ScrollCaptureResponse mScrollCaptureResponse; @@ -120,6 +121,7 @@ public class LongScreenshotActivity extends Activity { mSave = requireViewById(R.id.save); mEdit = requireViewById(R.id.edit); mShare = requireViewById(R.id.share); + mDelete = requireViewById(R.id.delete); mCropView = requireViewById(R.id.crop_view); mMagnifierView = requireViewById(R.id.magnifier); mCropView.setCropInteractionListener(mMagnifierView); @@ -130,6 +132,13 @@ public class LongScreenshotActivity extends Activity { mEdit.setOnClickListener(this::onClicked); mShare.setOnClickListener(this::onClicked); + // Only show the delete button if we have something to delete (should typically be the case) + if (getIntent().getData() != null) { + mDelete.setOnClickListener(this::onClicked); + } else { + mDelete.setVisibility(View.GONE); + } + mPreview.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateImageDimensions()); @@ -200,7 +209,6 @@ public class LongScreenshotActivity extends Activity { / (float) mLongScreenshot.getHeight()); mEnterTransitionView.setImageDrawable(drawable); - mEnterTransitionView.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override @@ -220,7 +228,6 @@ public class LongScreenshotActivity extends Activity { mCropView.animateEntrance(); mCropView.setVisibility(View.VISIBLE); setButtonsEnabled(true); - mEnterTransitionView.setVisibility(View.GONE); }); }); return true; @@ -228,8 +235,8 @@ public class LongScreenshotActivity extends Activity { }); // Immediately export to temp image file for saved state - mCacheSaveFuture = mImageExporter.exportAsTempFile(mBackgroundExecutor, - mLongScreenshot.toBitmap()); + mCacheSaveFuture = mImageExporter.exportToRawFile(mBackgroundExecutor, + mLongScreenshot.toBitmap(), new File(getCacheDir(), "long_screenshot_cache.png")); mCacheSaveFuture.addListener(() -> { try { // Get the temp file path to persist, used in onSavedInstanceState @@ -321,6 +328,7 @@ public class LongScreenshotActivity extends Activity { mSave.setEnabled(enabled); mEdit.setEnabled(enabled); mShare.setEnabled(enabled); + mDelete.setEnabled(enabled); } private void doEdit(Uri uri) { @@ -334,11 +342,18 @@ public class LongScreenshotActivity extends Activity { | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); mTransitionView.setImageBitmap(mOutputBitmap); - mTransitionView.setVisibility(View.VISIBLE); mTransitionView.setTransitionName( ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); // TODO: listen for transition completing instead of finishing onStop mTransitionStarted = true; + int[] locationOnScreen = new int[2]; + mTransitionView.getLocationOnScreen(locationOnScreen); + int[] locationInWindow = new int[2]; + mTransitionView.getLocationInWindow(locationInWindow); + int deltaX = locationOnScreen[0] - locationInWindow[0]; + int deltaY = locationOnScreen[1] - locationInWindow[1]; + mTransitionView.setX(mTransitionView.getX() - deltaX); + mTransitionView.setY(mTransitionView.getY() - deltaY); startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle()); @@ -368,6 +383,11 @@ public class LongScreenshotActivity extends Activity { } else if (id == R.id.share) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_SHARE); startExport(PendingAction.SHARE); + } else if (id == R.id.delete) { + mBackgroundExecutor.execute(() -> { + getContentResolver().delete(getIntent().getData(), null); + finishAndRemoveTask(); + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java index 34b40f79836b..78737329750a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java @@ -33,6 +33,7 @@ import android.view.ViewPropertyAnimator; import androidx.annotation.Nullable; +import com.android.internal.graphics.ColorUtils; import com.android.systemui.R; /** @@ -83,7 +84,9 @@ public class MagnifierView extends View implements CropView.CropInteractionListe TypedArray t = context.getTheme().obtainStyledAttributes( attrs, R.styleable.MagnifierView, 0, 0); mShadePaint = new Paint(); - mShadePaint.setColor(t.getColor(R.styleable.MagnifierView_scrimColor, Color.TRANSPARENT)); + int alpha = t.getInteger(R.styleable.MagnifierView_scrimAlpha, 255); + int scrimColor = t.getColor(R.styleable.MagnifierView_scrimColor, Color.TRANSPARENT); + mShadePaint.setColor(ColorUtils.setAlphaComponent(scrimColor, alpha)); mHandlePaint = new Paint(); mHandlePaint.setColor(t.getColor(R.styleable.MagnifierView_handleColor, Color.BLACK)); mHandlePaint.setStrokeWidth( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 2e138362aedb..52b393f563b6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -16,6 +16,7 @@ package com.android.systemui.screenshot; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -33,6 +34,7 @@ import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ExitTransitionCoordinator; import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; @@ -253,13 +255,16 @@ public class ScreenshotController { private final DisplayManager mDisplayManager; private final ScrollCaptureController mScrollCaptureController; private final LongScreenshotData mLongScreenshotHolder; + private final boolean mIsLowRamDevice; private ScreenshotView mScreenshotView; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; + private boolean mScreenshotTakenInPortrait; private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; + private Uri mLatestUriSaved; private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { @Override @@ -297,7 +302,8 @@ public class ScreenshotController { ImageExporter imageExporter, @Main Executor mainExecutor, ScrollCaptureController scrollCaptureController, - LongScreenshotData longScreenshotHolder) { + LongScreenshotData longScreenshotHolder, + ActivityManager activityManager) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; mScrollCaptureClient = scrollCaptureClient; @@ -306,6 +312,7 @@ public class ScreenshotController { mMainExecutor = mainExecutor; mScrollCaptureController = scrollCaptureController; mLongScreenshotHolder = longScreenshotHolder; + mIsLowRamDevice = activityManager.isLowRamDevice(); mBgExecutor = Executors.newSingleThreadExecutor(); mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); @@ -484,11 +491,29 @@ public class ScreenshotController { * Takes a screenshot of the current display and shows an animation. */ private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) { + mScreenshotTakenInPortrait = + mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; + // copy the input Rect, since SurfaceControl.screenshot can mutate it Rect screenRect = new Rect(crop); + Bitmap screenshot = captureScreenshot(crop); + + if (screenshot == null) { + Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + if (mCurrentRequestCallback != null) { + mCurrentRequestCallback.reportError(); + } + return; + } + + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); + } + + private Bitmap captureScreenshot(Rect crop) { int width = crop.width(); int height = crop.height(); - Bitmap screenshot = null; final Display display = getDefaultDisplay(); final DisplayAddress address = display.getAddress(); @@ -509,18 +534,7 @@ public class ScreenshotController { SurfaceControl.captureDisplay(captureArgs); screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); } - - if (screenshot == null) { - Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null"); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - if (mCurrentRequestCallback != null) { - mCurrentRequestCallback.reportError(); - } - return; - } - - saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); + return screenshot; } private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, @@ -533,7 +547,6 @@ public class ScreenshotController { mAccessibilityManager.sendAccessibilityEvent(event); } - if (mScreenshotView.isAttachedToWindow()) { // if we didn't already dismiss for another reason if (!mScreenshotView.isDismissing()) { @@ -550,6 +563,7 @@ public class ScreenshotController { .getWindowInsets().getDisplayCutout()); mScreenBitmap = screenshot; + mLatestUriSaved = null; if (!isUserSetupComplete()) { Log.w(TAG, "User setup not complete, displaying toast only"); @@ -617,6 +631,10 @@ public class ScreenshotController { } private void requestScrollCapture() { + if (!allowLongScreenshots()) { + Log.d(TAG, "Long screenshots not supported on this device"); + return; + } mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken()); if (mLastScrollCaptureRequest != null) { mLastScrollCaptureRequest.cancel(true); @@ -644,45 +662,61 @@ public class ScreenshotController { final ScrollCaptureResponse response = mLastScrollCaptureResponse; mScreenshotView.showScrollChip(/* onClick */ () -> { - mScreenshotView.prepareScrollingTransition(response, mScreenBitmap); - // Clear the reference to prevent close() in dismissScreenshot - mLastScrollCaptureResponse = null; - final ListenableFuture<ScrollCaptureController.LongScreenshot> future = - mScrollCaptureController.run(response); - future.addListener(() -> { - ScrollCaptureController.LongScreenshot longScreenshot; - try { - longScreenshot = future.get(); - } catch (CancellationException | InterruptedException | ExecutionException e) { - Log.e(TAG, "Exception", e); - return; - } + DisplayMetrics displayMetrics = new DisplayMetrics(); + getDefaultDisplay().getRealMetrics(displayMetrics); + Bitmap newScreenshot = captureScreenshot( + new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); + + mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, + mScreenshotTakenInPortrait); + // delay starting scroll capture to make sure the scrim is up before the app moves + mScreenshotView.post(() -> { + // Clear the reference to prevent close() in dismissScreenshot + mLastScrollCaptureResponse = null; + final ListenableFuture<ScrollCaptureController.LongScreenshot> future = + mScrollCaptureController.run(response); + future.addListener(() -> { + ScrollCaptureController.LongScreenshot longScreenshot; + + try { + longScreenshot = future.get(); + } catch (CancellationException + | InterruptedException + | ExecutionException e) { + Log.e(TAG, "Exception", e); + mScreenshotView.restoreNonScrollingUi(); + return; + } - mLongScreenshotHolder.setLongScreenshot(longScreenshot); - mLongScreenshotHolder.setTransitionDestinationCallback( - (transitionDestination, onTransitionEnd) -> - mScreenshotView.startLongScreenshotTransition( - transitionDestination, onTransitionEnd, - longScreenshot)); - - final Intent intent = new Intent(mContext, LongScreenshotActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - - Pair<ActivityOptions, ExitTransitionCoordinator> transition = - ActivityOptions.startSharedElementAnimation(mWindow, - new ScreenshotExitTransitionCallbacksSupplier(false).get(), - null); - transition.second.startExit(); - mContext.startActivity(intent, transition.first.toBundle()); - RemoteAnimationAdapter runner = new RemoteAnimationAdapter( - SCREENSHOT_REMOTE_RUNNER, 0, 0); - try { - WindowManagerGlobal.getWindowManagerService() - .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY); - } catch (Exception e) { - Log.e(TAG, "Error overriding screenshot app transition", e); - } - }, mMainExecutor); + if (longScreenshot.getHeight() == 0) { + mScreenshotView.restoreNonScrollingUi(); + return; + } + + mLongScreenshotHolder.setLongScreenshot(longScreenshot); + mLongScreenshotHolder.setTransitionDestinationCallback( + (transitionDestination, onTransitionEnd) -> + mScreenshotView.startLongScreenshotTransition( + transitionDestination, onTransitionEnd, + longScreenshot)); + + final Intent intent = new Intent(mContext, LongScreenshotActivity.class); + intent.setData(mLatestUriSaved); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + mContext.startActivity(intent, + ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle()); + RemoteAnimationAdapter runner = new RemoteAnimationAdapter( + SCREENSHOT_REMOTE_RUNNER, 0, 0); + try { + WindowManagerGlobal.getWindowManagerService() + .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY); + } catch (Exception e) { + Log.e(TAG, "Error overriding screenshot app transition", e); + } + }, mMainExecutor); + }); }); } catch (CancellationException e) { // Ignore @@ -857,6 +891,8 @@ public class ScreenshotController { resetTimeout(); + mLatestUriSaved = imageData.uri; + if (imageData.uri != null) { mScreenshotHandler.post(() -> { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { @@ -904,10 +940,12 @@ public class ScreenshotController { */ private Supplier<ActionTransition> getActionTransitionSupplier() { return () -> { + View preview = mScreenshotView.getTransitionView(); + preview.setX(preview.getX() - mScreenshotView.getStaticLeftMargin()); Pair<ActivityOptions, ExitTransitionCoordinator> transition = ActivityOptions.startSharedElementAnimation( mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(), - null, Pair.create(mScreenshotView.getScreenshotPreview(), + null, Pair.create(mScreenshotView.getTransitionView(), ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); transition.second.startExit(); @@ -967,6 +1005,10 @@ public class ScreenshotController { return mDisplayManager.getDisplay(DEFAULT_DISPLAY); } + private boolean allowLongScreenshots() { + return !mIsLowRamDevice; + } + /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 669046591170..e9e62f26a10e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -36,8 +36,10 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Matrix; @@ -135,11 +137,13 @@ public class ScreenshotView extends FrameLayout implements private int mNavMode; private boolean mOrientationPortrait; private boolean mDirectionLTR; + private int mStaticLeftMargin; private ScreenshotSelectorView mScreenshotSelectorView; private ImageView mScrollingScrim; private View mScreenshotStatic; private ImageView mScreenshotPreview; + private View mTransitionView; private View mScreenshotPreviewBorder; private ImageView mScrollablePreview; private ImageView mScreenshotFlash; @@ -160,6 +164,7 @@ public class ScreenshotView extends FrameLayout implements private GestureDetector mSwipeDetector; private SwipeDismissHandler mSwipeDismissHandler; private InputMonitorCompat mInputMonitor; + private boolean mShowScrollablePreview; private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>(); private PendingInteraction mPendingInteraction; @@ -257,10 +262,10 @@ public class ScreenshotView extends FrameLayout implements @Override // ViewTreeObserver.OnComputeInternalInsetsListener public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(getTouchRegion()); + inoutInfo.touchableRegion.set(getTouchRegion(true)); } - private Region getTouchRegion() { + private Region getTouchRegion(boolean includeScrim) { Region touchRegion = new Region(); final Rect tmpRect = new Rect(); @@ -273,6 +278,11 @@ public class ScreenshotView extends FrameLayout implements mDismissButton.getBoundsOnScreen(tmpRect); touchRegion.op(tmpRect, Region.Op.UNION); + if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) { + mScrollingScrim.getBoundsOnScreen(tmpRect); + touchRegion.op(tmpRect, Region.Op.UNION); + } + if (QuickStepContract.isGesturalMode(mNavMode)) { final WindowManager wm = mContext.getSystemService(WindowManager.class); final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); @@ -296,7 +306,7 @@ public class ScreenshotView extends FrameLayout implements if (ev instanceof MotionEvent) { MotionEvent event = (MotionEvent) ev; if (event.getActionMasked() == MotionEvent.ACTION_DOWN - && !getTouchRegion().contains( + && !getTouchRegion(false).contains( (int) event.getRawX(), (int) event.getRawY())) { mCallbacks.onTouchOutside(); } @@ -313,6 +323,10 @@ public class ScreenshotView extends FrameLayout implements @Override // ViewGroup public boolean onInterceptTouchEvent(MotionEvent ev) { + // scrolling scrim should not be swipeable; return early if we're on the scrim + if (!getTouchRegion(false).contains((int) ev.getRawX(), (int) ev.getRawY())) { + return false; + } // always pass through the down event so the swipe handler knows the initial state if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { mSwipeDismissHandler.onTouch(this, ev); @@ -325,6 +339,7 @@ public class ScreenshotView extends FrameLayout implements mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim)); mScreenshotStatic = requireNonNull(findViewById(R.id.global_screenshot_static)); mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview)); + mTransitionView = requireNonNull(findViewById(R.id.screenshot_transition_view)); mScreenshotPreviewBorder = requireNonNull( findViewById(R.id.global_screenshot_preview_border)); mScreenshotPreview.setClipToOutline(true); @@ -370,12 +385,12 @@ public class ScreenshotView extends FrameLayout implements requestFocus(); } - View getScreenshotPreview() { - return mScreenshotPreview; + View getTransitionView() { + return mTransitionView; } - View getScrollablePreview() { - return mScrollablePreview; + int getStaticLeftMargin() { + return mStaticLeftMargin; } /** @@ -416,6 +431,7 @@ public class ScreenshotView extends FrameLayout implements Math.max(cutout.getSafeInsetRight(), waterfall.right), waterfall.bottom); } } + mStaticLeftMargin = p.leftMargin; mScreenshotStatic.setLayoutParams(p); mScreenshotStatic.requestLayout(); } @@ -453,14 +469,6 @@ public class ScreenshotView extends FrameLayout implements mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); final float currentScale = 1 / cornerScale; - mScreenshotPreview.setScaleX(currentScale); - mScreenshotPreview.setScaleY(currentScale); - - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setAlpha(0); - mDismissButton.setVisibility(View.VISIBLE); - } - AnimatorSet dropInAnimation = new AnimatorSet(); ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); @@ -491,6 +499,20 @@ public class ScreenshotView extends FrameLayout implements ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); + + toCorner.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mScreenshotPreview.setScaleX(currentScale); + mScreenshotPreview.setScaleY(currentScale); + mScreenshotPreview.setVisibility(View.VISIBLE); + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setAlpha(0); + mDismissButton.setVisibility(View.VISIBLE); + } + } + }); + float xPositionPct = SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; float dismissPct = @@ -534,13 +556,6 @@ public class ScreenshotView extends FrameLayout implements } }); - toCorner.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mScreenshotPreview.setVisibility(View.VISIBLE); - } - }); - mScreenshotFlash.setAlpha(0f); mScreenshotFlash.setVisibility(View.VISIBLE); @@ -766,70 +781,130 @@ public class ScreenshotView extends FrameLayout implements void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd, ScrollCaptureController.LongScreenshot longScreenshot) { - mScrollablePreview.setImageBitmap(longScreenshot.toBitmap()); - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - float startX = mScrollablePreview.getX(); - float startY = mScrollablePreview.getY(); - int[] locInScreen = mScrollablePreview.getLocationOnScreen(); - destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]); - mScrollablePreview.setPivotX(0); - mScrollablePreview.setPivotY(0); - mScrollablePreview.setAlpha(1f); - float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth(); - Matrix matrix = new Matrix(); - matrix.setScale(currentScale, currentScale); - matrix.postTranslate( - longScreenshot.getLeft() * currentScale, longScreenshot.getTop() * currentScale); - mScrollablePreview.setImageMatrix(matrix); - float destinationScale = destination.width() / (float) mScrollablePreview.getWidth(); - anim.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - mScrollingScrim.setAlpha(1 - t); - float currScale = MathUtils.lerp(1, destinationScale, t); - mScrollablePreview.setScaleX(currScale); - mScrollablePreview.setScaleY(currScale); - mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t)); - mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t)); - }); - anim.addListener(new AnimatorListenerAdapter() { + AnimatorSet animSet = new AnimatorSet(); + + ValueAnimator scrimAnim = ValueAnimator.ofFloat(0, 1); + scrimAnim.addUpdateListener(animation -> + mScrollingScrim.setAlpha(1 - animation.getAnimatedFraction())); + + if (mShowScrollablePreview) { + mScrollablePreview.setImageBitmap(longScreenshot.toBitmap()); + float startX = mScrollablePreview.getX(); + float startY = mScrollablePreview.getY(); + int[] locInScreen = mScrollablePreview.getLocationOnScreen(); + destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]); + mScrollablePreview.setPivotX(0); + mScrollablePreview.setPivotY(0); + mScrollablePreview.setAlpha(1f); + float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth(); + Matrix matrix = new Matrix(); + matrix.setScale(currentScale, currentScale); + matrix.postTranslate( + longScreenshot.getLeft() * currentScale, + longScreenshot.getTop() * currentScale); + mScrollablePreview.setImageMatrix(matrix); + float destinationScale = destination.width() / (float) mScrollablePreview.getWidth(); + + ValueAnimator previewAnim = ValueAnimator.ofFloat(0, 1); + previewAnim.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + float currScale = MathUtils.lerp(1, destinationScale, t); + mScrollablePreview.setScaleX(currScale); + mScrollablePreview.setScaleY(currScale); + mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t)); + mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t)); + }); + ValueAnimator previewFadeAnim = ValueAnimator.ofFloat(1, 0); + previewFadeAnim.addUpdateListener(animation -> + mScrollablePreview.setAlpha(1 - animation.getAnimatedFraction())); + animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim); + previewAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + onTransitionEnd.run(); + } + }); + } else { + // if we switched orientations between the original screenshot and the long screenshot + // capture, just fade out the scrim instead of running the preview animation + animSet.play(scrimAnim); + animSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + onTransitionEnd.run(); + } + }); + } + animSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - onTransitionEnd.run(); - mScrollablePreview.animate().alpha(0).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); mCallbacks.onDismiss(); } - }); - } }); - anim.start(); + animSet.start(); } - void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap) { - mScrollingScrim.setImageBitmap(screenBitmap); + void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap, + Bitmap newBitmap, boolean screenshotTakenInPortrait) { + mShowScrollablePreview = (screenshotTakenInPortrait == mOrientationPortrait); + + mScrollingScrim.setImageBitmap(newBitmap); mScrollingScrim.setVisibility(View.VISIBLE); - Rect scrollableArea = scrollableAreaOnScreen(response); - float scale = mCornerSizeX - / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight()); - ConstraintLayout.LayoutParams params = - (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams(); - - params.width = (int) (scale * scrollableArea.width()); - params.height = (int) (scale * scrollableArea.height()); - Matrix matrix = new Matrix(); - matrix.setScale(scale, scale); - matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale); - - mScrollablePreview.setTranslationX(scale * scrollableArea.left); - mScrollablePreview.setTranslationY(scale * scrollableArea.top); - mScrollablePreview.setImageMatrix(matrix); - - mScrollablePreview.setImageBitmap(screenBitmap); - mScrollablePreview.setVisibility(View.VISIBLE); - createScreenshotFadeDismissAnimation(true).start(); + + if (mShowScrollablePreview) { + Rect scrollableArea = scrollableAreaOnScreen(response); + + float scale = mCornerSizeX + / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight()); + ConstraintLayout.LayoutParams params = + (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams(); + + params.width = (int) (scale * scrollableArea.width()); + params.height = (int) (scale * scrollableArea.height()); + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); + matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale); + + mScrollablePreview.setTranslationX(scale + * (mDirectionLTR ? scrollableArea.left : scrollableArea.right - getWidth())); + mScrollablePreview.setTranslationY(scale * scrollableArea.top); + mScrollablePreview.setImageMatrix(matrix); + mScrollablePreview.setImageBitmap(screenBitmap); + mScrollablePreview.setVisibility(View.VISIBLE); + } + mDismissButton.setVisibility(View.GONE); + mActionsContainer.setVisibility(View.GONE); + mBackgroundProtection.setVisibility(View.GONE); + // set these invisible, but not gone, so that the views are laid out correctly + mActionsContainerBackground.setVisibility(View.INVISIBLE); + mScreenshotPreviewBorder.setVisibility(View.INVISIBLE); + mScreenshotPreview.setVisibility(View.INVISIBLE); + mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP); + ValueAnimator anim = ValueAnimator.ofFloat(0, .3f); + anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList( + ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0)))); + anim.setDuration(200); + anim.start(); + } + + void restoreNonScrollingUi() { + mScrollChip.setVisibility(View.GONE); + mScrollablePreview.setVisibility(View.GONE); + mScrollingScrim.setVisibility(View.GONE); + + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + mActionsContainer.setVisibility(View.VISIBLE); + mBackgroundProtection.setVisibility(View.VISIBLE); + mActionsContainerBackground.setVisibility(View.VISIBLE); + mScreenshotPreviewBorder.setVisibility(View.VISIBLE); + mScreenshotPreview.setVisibility(View.VISIBLE); + // reset the timeout + mCallbacks.onUserInteraction(); } boolean isDismissing() { @@ -845,10 +920,6 @@ public class ScreenshotView extends FrameLayout implements } private void animateDismissal(Animator dismissAnimation) { - if (DEBUG_WINDOW) { - Log.d(TAG, "removing OnComputeInternalInsetsListener"); - } - getViewTreeObserver().removeOnComputeInternalInsetsListener(this); mDismissAnimation = dismissAnimation; mDismissAnimation.addListener(new AnimatorListenerAdapter() { private boolean mCancelled = false; @@ -904,12 +975,15 @@ public class ScreenshotView extends FrameLayout implements mActionsContainer.setVisibility(View.GONE); mBackgroundProtection.setAlpha(0f); mDismissButton.setVisibility(View.GONE); + mScrollingScrim.setVisibility(View.GONE); + mScrollablePreview.setVisibility(View.GONE); mScreenshotStatic.setTranslationX(0); mScreenshotPreview.setTranslationY(0); mScreenshotPreview.setContentDescription( mContext.getResources().getString(R.string.screenshot_preview_description)); mScreenshotPreview.setOnClickListener(null); mShareChip.setOnClickListener(null); + mScrollingScrim.setVisibility(View.GONE); mEditChip.setOnClickListener(null); mShareChip.setIsPending(false); mEditChip.setIsPending(false); @@ -932,7 +1006,7 @@ public class ScreenshotView extends FrameLayout implements transition.action.actionIntent.send(); // fade out non-preview UI - createScreenshotFadeDismissAnimation(false).start(); + createScreenshotFadeDismissAnimation().start(); } catch (PendingIntent.CanceledException e) { mPendingSharedTransition = false; if (transition.onCancelRunnable != null) { @@ -970,7 +1044,7 @@ public class ScreenshotView extends FrameLayout implements return animSet; } - ValueAnimator createScreenshotFadeDismissAnimation(boolean fadePreview) { + ValueAnimator createScreenshotFadeDismissAnimation() { ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); alphaAnim.addUpdateListener(animation -> { float alpha = 1 - animation.getAnimatedFraction(); @@ -979,9 +1053,6 @@ public class ScreenshotView extends FrameLayout implements mActionsContainer.setAlpha(alpha); mBackgroundProtection.setAlpha(alpha); mScreenshotPreviewBorder.setAlpha(alpha); - if (fadePreview) { - mScreenshotPreview.setAlpha(alpha); - } }); alphaAnim.setDuration(600); return alphaAnim; diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt index e6d48676dc03..24775344240a 100644 --- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt @@ -23,6 +23,7 @@ import android.content.res.Resources import android.hardware.SensorPrivacyManager import android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS import android.hardware.SensorPrivacyManager.EXTRA_SENSOR +import android.hardware.SensorPrivacyManager.Sources.DIALOG import android.os.Bundle import android.os.Handler import android.text.Html @@ -32,6 +33,7 @@ import android.widget.ImageView import com.android.internal.app.AlertActivity import com.android.internal.widget.DialogTitle import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import com.android.systemui.statusbar.policy.KeyguardStateController @@ -46,13 +48,15 @@ import javax.inject.Inject class SensorUseStartedActivity @Inject constructor( private val sensorPrivacyController: IndividualSensorPrivacyController, private val keyguardStateController: KeyguardStateController, - private val keyguardDismissUtil: KeyguardDismissUtil + private val keyguardDismissUtil: KeyguardDismissUtil, + @Background private val bgHandler: Handler ) : AlertActivity(), DialogInterface.OnClickListener { companion object { private val LOG_TAG = SensorUseStartedActivity::class.java.simpleName private const val SUPPRESS_REMINDERS_REMOVAL_DELAY_MILLIS = 2000L + private const val UNLOCK_DELAY_MILLIS = 200L private const val CAMERA = SensorPrivacyManager.Sensors.CAMERA private const val MICROPHONE = SensorPrivacyManager.Sensors.MICROPHONE @@ -179,9 +183,12 @@ class SensorUseStartedActivity @Inject constructor( BUTTON_POSITIVE -> { if (keyguardStateController.isMethodSecure && keyguardStateController.isShowing) { keyguardDismissUtil.executeWhenUnlocked({ - disableSensorPrivacy() + bgHandler.postDelayed({ + disableSensorPrivacy() + }, UNLOCK_DELAY_MILLIS) + false - }, false, false) + }, false, true) } else { disableSensorPrivacy() } @@ -201,7 +208,7 @@ class SensorUseStartedActivity @Inject constructor( sensorPrivacyController .suppressSensorPrivacyReminders(sensorUsePackageName, false) } else { - Handler(mainLooper).postDelayed({ + bgHandler.postDelayed({ sensorPrivacyController .suppressSensorPrivacyReminders(sensorUsePackageName, false) }, SUPPRESS_REMINDERS_REMOVAL_DELAY_MILLIS) @@ -219,10 +226,10 @@ class SensorUseStartedActivity @Inject constructor( private fun disableSensorPrivacy() { if (sensor == ALL_SENSORS) { - sensorPrivacyController.setSensorBlocked(MICROPHONE, false) - sensorPrivacyController.setSensorBlocked(CAMERA, false) + sensorPrivacyController.setSensorBlocked(DIALOG, MICROPHONE, false) + sensorPrivacyController.setSensorBlocked(DIALOG, CAMERA, false) } else { - sensorPrivacyController.setSensorBlocked(sensor, false) + sensorPrivacyController.setSensorBlocked(DIALOG, sensor, false) } unsuppressImmediately = true setResult(RESULT_OK) diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java index 9d101effa99f..8cd3632b65ba 100644 --- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java +++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java @@ -18,6 +18,7 @@ package com.android.systemui.sensorprivacy.television; import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; +import static android.hardware.SensorPrivacyManager.Sources.OTHER; import android.hardware.SensorPrivacyManager; import android.os.Bundle; @@ -119,10 +120,10 @@ public class TvUnblockSensorActivity extends TvBottomSheetActivity { com.android.internal.R.string.sensor_privacy_start_use_dialog_turn_on_button); unblockButton.setOnClickListener(v -> { if (mSensor == ALL_SENSORS) { - mSensorPrivacyController.setSensorBlocked(CAMERA, false); - mSensorPrivacyController.setSensorBlocked(MICROPHONE, false); + mSensorPrivacyController.setSensorBlocked(OTHER, CAMERA, false); + mSensorPrivacyController.setSensorBlocked(OTHER, MICROPHONE, false); } else { - mSensorPrivacyController.setSensorBlocked(mSensor, false); + mSensorPrivacyController.setSensorBlocked(OTHER, mSensor, false); } }); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java index 8dd6c8926434..15aa2b730adf 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java @@ -180,6 +180,8 @@ public class BrightnessSliderView extends FrameLayout { * Sets the scale for the progress bar (for brightness_progress_drawable.xml) * * This will only scale the thick progress bar and not the icon inside + * + * Used in {@link com.android.systemui.qs.QSAnimator}. */ public void setSliderScaleY(float scale) { if (scale != mScale) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 89dda9c52651..aafeabc7c1a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -20,6 +20,7 @@ import android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED import android.app.ActivityManager import android.content.res.Resources +import android.os.SystemProperties import android.util.IndentingPrintWriter import android.util.MathUtils import android.view.CrossWindowBlurListeners @@ -100,7 +101,8 @@ open class BlurUtils @Inject constructor( */ open fun supportsBlursOnWindows(): Boolean { return CROSS_WINDOW_BLUR_SUPPORTED && ActivityManager.isHighEndGfx() && - crossWindowBlurListeners.isCrossWindowBlurEnabled() + crossWindowBlurListeners.isCrossWindowBlurEnabled() && + !SystemProperties.getBoolean("persist.sysui.disableBlur", false) } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index 7e676197ddad..5a4245853a6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar; +import android.content.Context; +import android.util.FeatureFlagUtils; + import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.flags.FeatureFlagReader; @@ -30,10 +33,12 @@ import javax.inject.Inject; @SysUISingleton public class FeatureFlags { private final FeatureFlagReader mFlagReader; + private final Context mContext; @Inject - public FeatureFlags(FeatureFlagReader flagReader) { + public FeatureFlags(FeatureFlagReader flagReader, Context context) { mFlagReader = flagReader; + mContext = context; } public boolean isNewNotifPipelineEnabled() { @@ -84,4 +89,27 @@ public class FeatureFlags { public boolean isSmartspaceDedupingEnabled() { return isSmartspaceEnabled() && mFlagReader.isEnabled(R.bool.flag_smartspace_deduping); } + + public boolean isNewKeyguardSwipeAnimationEnabled() { + return mFlagReader.isEnabled(R.bool.flag_new_unlock_swipe_animation); + } + + public boolean isSmartSpaceSharedElementTransitionEnabled() { + return mFlagReader.isEnabled(R.bool.flag_smartspace_shared_element_transition); + } + + /** Whether or not to use the provider model behavior for the status bar icons */ + public boolean isCombinedStatusBarSignalIconsEnabled() { + return mFlagReader.isEnabled(R.bool.flag_combined_status_bar_signal_icons); + } + + /** System setting for provider model behavior */ + public boolean isProviderModelSettingEnabled() { + return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); + } + + /** static method for the system setting */ + public static boolean isProviderModelSettingEnabled(Context context) { + return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 0bb702f6c9e4..44399a126624 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -66,7 +66,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.ViewClippingUtil; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; @@ -130,7 +129,6 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal private String mRestingIndication; private String mAlignmentIndication; private CharSequence mTransientIndication; - private boolean mTransientTextIsError; protected ColorStateList mInitialTextColorState; private boolean mVisible; private boolean mHideTransientMessageOnScreenOff; @@ -382,8 +380,7 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal private void updateTransient() { if (!TextUtils.isEmpty(mTransientIndication)) { - mRotateTextViewController.showTransient(mTransientIndication, - mTransientTextIsError); + mRotateTextViewController.showTransient(mTransientIndication); } else { mRotateTextViewController.hideTransient(); } @@ -421,7 +418,8 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal INDICATION_TYPE_ALIGNMENT, new KeyguardIndication.Builder() .setMessage(mAlignmentIndication) - .setTextColor(Utils.getColorError(mContext)) + .setTextColor(ColorStateList.valueOf( + mContext.getColor(R.color.misalignment_text_color))) .build(), true); } else { @@ -594,14 +592,13 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal boolean isError, boolean hideOnScreenOff) { mTransientIndication = transientIndication; mHideTransientMessageOnScreenOff = hideOnScreenOff && transientIndication != null; - mTransientTextIsError = isError; mHandler.removeMessages(MSG_HIDE_TRANSIENT); mHandler.removeMessages(MSG_SWIPE_UP_TO_UNLOCK); if (mDozing && !TextUtils.isEmpty(mTransientIndication)) { // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared. mWakeLock.setAcquired(true); - hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS); } + hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS); updateIndication(false); } @@ -800,18 +797,20 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal } if (mStatusBarKeyguardViewManager.isBouncerShowing()) { - String message = mContext.getString(R.string.keyguard_retry); - mStatusBarKeyguardViewManager.showBouncerMessage(message, mInitialTextColorState); + if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) { + return; // udfps affordance is highlighted, no need to surface face auth error + } else { + String message = mContext.getString(R.string.keyguard_retry); + mStatusBarKeyguardViewManager.showBouncerMessage(message, mInitialTextColorState); + } } else if (mKeyguardUpdateMonitor.isScreenOn()) { showTransientIndication(mContext.getString(R.string.keyguard_unlock), false /* isError */, true /* hideOnScreenOff */); - hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS); } } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("KeyguardIndicationController:"); - pw.println(" mTransientTextIsError: " + mTransientTextIsError); pw.println(" mInitialTextColorState: " + mInitialTextColorState); pw.println(" mPowerPluggedInWired: " + mPowerPluggedInWired); pw.println(" mPowerPluggedIn: " + mPowerPluggedIn); @@ -866,7 +865,6 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal if (mDozing) { if (!wasPluggedIn && mPowerPluggedIn) { showTransientIndication(computePowerIndication()); - hideTransientIndicationDelayed(HIDE_DELAY_MS); } else if (wasPluggedIn && !mPowerPluggedIn) { hideTransientIndication(); } @@ -885,16 +883,20 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)) { return; } + boolean showSwipeToUnlock = msgId == KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.showBouncerMessage(helpString, mInitialTextColorState); } else if (mKeyguardUpdateMonitor.isScreenOn()) { - showTransientIndication(helpString, false /* isError */, showSwipeToUnlock); - if (!showSwipeToUnlock) { - hideTransientIndicationDelayed(TRANSIENT_BIOMETRIC_ERROR_TIMEOUT); + if (biometricSourceType == BiometricSourceType.FACE + && shouldSuppressFaceMsgAndShowTryFingerprintMsg()) { + // suggest trying fingerprint + showTransientIndication(R.string.keyguard_try_fingerprint); + return; } + showTransientIndication(helpString, false /* isError */, showSwipeToUnlock); } if (showSwipeToUnlock) { mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SWIPE_UP_TO_UNLOCK), @@ -908,13 +910,27 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal if (shouldSuppressBiometricError(msgId, biometricSourceType, mKeyguardUpdateMonitor)) { return; } + if (biometricSourceType == BiometricSourceType.FACE + && shouldSuppressFaceMsgAndShowTryFingerprintMsg() + && !mStatusBarKeyguardViewManager.isBouncerShowing() + && mKeyguardUpdateMonitor.isScreenOn()) { + // suggest trying fingerprint + showTransientIndication(R.string.keyguard_try_fingerprint); + return; + } if (msgId == FaceManager.FACE_ERROR_TIMEOUT) { // The face timeout message is not very actionable, let's ask the user to // manually retry. if (!mStatusBarKeyguardViewManager.isBouncerShowing() - && mKeyguardUpdateMonitor.isUdfpsEnrolled()) { + && mKeyguardUpdateMonitor.isUdfpsEnrolled() + && mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { // suggest trying fingerprint showTransientIndication(R.string.keyguard_try_fingerprint); + } else if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) { + mStatusBarKeyguardViewManager.showBouncerMessage( + mContext.getResources().getString(R.string.keyguard_try_fingerprint), + mInitialTextColorState + ); } else { // suggest swiping up to unlock (try face auth again or swipe up to bouncer) showSwipeUpToUnlock(); @@ -924,8 +940,6 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal } else if (mKeyguardUpdateMonitor.isScreenOn()) { showTransientIndication(errString, /* isError */ true, /* hideOnScreenOff */ true); - // We want to keep this message around in case the screen was off - hideTransientIndicationDelayed(HIDE_DELAY_MS); } else { mMessageToShowOnScreenOn = errString; } @@ -952,6 +966,15 @@ public class KeyguardIndicationController implements KeyguardStateController.Cal || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED); } + private boolean shouldSuppressFaceMsgAndShowTryFingerprintMsg() { + // For dual biometric, don't show face auth messages unless face auth was explicitly + // requested by the user. + return mKeyguardUpdateMonitor.isFingerprintDetectionRunning() + && !mKeyguardUpdateMonitor.isFaceAuthUserRequested() + && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + true /* isStrongBiometric */); + } + private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) { // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index ec648ad519a3..538db6168408 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -14,6 +14,7 @@ import android.graphics.Shader import android.util.AttributeSet import android.view.View import com.android.systemui.animation.Interpolators +import java.util.function.Consumer /** * Provides methods to modify the various properties of a [LightRevealScrim] to reveal between 0% to @@ -51,7 +52,7 @@ object LiftReveal : LightRevealEffect { private const val OVAL_INITIAL_WIDTH_PERCENT = 0.5f /** The initial top value of the light oval, in percent of scrim height. */ - private const val OVAL_INITIAL_TOP_PERCENT = 1.05f + private const val OVAL_INITIAL_TOP_PERCENT = 1.1f /** The initial bottom value of the light oval, in percent of scrim height. */ private const val OVAL_INITIAL_BOTTOM_PERCENT = 1.2f @@ -148,6 +149,8 @@ class PowerButtonReveal( */ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + lateinit var revealAmountListener: Consumer<Float> + /** * How much of the underlying views are revealed, in percent. 0 means they will be completely * obscured and 1 means they'll be fully visible. @@ -158,6 +161,7 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, field = value revealEffect.setRevealAmountOnScrim(value, this) + revealAmountListener.accept(value) invalidate() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 0b67e7eeb132..4552138761c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -369,7 +369,7 @@ public class NotificationRemoteInputManager implements Dumpable { }); mSmartReplyController.setCallback((entry, reply) -> { StatusBarNotification newSbn = - rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */, + rebuildNotificationWithRemoteInputInserted(entry, reply, true /* showSpinner */, null /* mimeType */, null /* uri */); mEntryManager.updateNotification(newSbn, null /* ranking */); }); @@ -638,12 +638,12 @@ public class NotificationRemoteInputManager implements Dumpable { @VisibleForTesting StatusBarNotification rebuildNotificationForCanceledSmartReplies( NotificationEntry entry) { - return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */, + return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */, false /* showSpinner */, null /* mimeType */, null /* uri */); } @VisibleForTesting - StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry, + StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry, CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) { StatusBarNotification sbn = entry.getSbn(); @@ -746,7 +746,7 @@ public class NotificationRemoteInputManager implements Dumpable { } String remoteInputMimeType = entry.remoteInputMimeType; Uri remoteInputUri = entry.remoteInputUri; - StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry, + StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry, remoteInputText, false /* showSpinner */, remoteInputMimeType, remoteInputUri); entry.onRemoteInputInserted(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 452d69369d71..28bdd5fbeb8b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.app.WallpaperManager import android.os.SystemClock +import android.os.Trace import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils @@ -93,8 +94,13 @@ class NotificationShadeDepthController @Inject constructor( var shadeAnimation = DepthAnimation() @VisibleForTesting - var globalActionsSpring = DepthAnimation() - var showingHomeControls: Boolean = false + var brightnessMirrorSpring = DepthAnimation() + var brightnessMirrorVisible: Boolean = false + set(value) { + field = value + brightnessMirrorSpring.animateTo(if (value) blurUtils.blurRadiusOfRatio(1f) + else 0) + } var qsPanelExpansion = 0f set(value) { @@ -117,7 +123,7 @@ class NotificationShadeDepthController @Inject constructor( * When launching an app from the shade, the animations progress should affect how blurry the * shade is, overriding the expansion amount. */ - var ignoreShadeBlurUntilHidden: Boolean = false + var blursDisabledForAppLaunch: Boolean = false set(value) { if (field == value) { return @@ -128,6 +134,10 @@ class NotificationShadeDepthController @Inject constructor( if (shadeSpring.radius == 0 && shadeAnimation.radius == 0) { return } + // Do not remove blurs when we're re-enabling them + if (!value) { + return + } shadeSpring.animateTo(0) shadeSpring.finishIfRunning() @@ -169,30 +179,28 @@ class NotificationShadeDepthController @Inject constructor( combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress)) var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius).toFloat() - if (ignoreShadeBlurUntilHidden) { - if (shadeRadius == 0f) { - ignoreShadeBlurUntilHidden = false - } else { - shadeRadius = 0f - } + if (blursDisabledForAppLaunch) { + shadeRadius = 0f } - // Home controls have black background, this means that we should not have blur when they - // are fully visible, otherwise we'll enter Client Composition unnecessarily. - var globalActionsRadius = globalActionsSpring.radius - if (showingHomeControls) { - globalActionsRadius = 0 - } - var blur = max(shadeRadius.toInt(), globalActionsRadius) + var blur = shadeRadius.toInt() // Make blur be 0 if it is necessary to stop blur effect. - if (scrimsVisible || !blurUtils.supportsBlursOnWindows()) { + if (scrimsVisible) { blur = 0 } + val zoomOut = blurUtils.ratioOfBlurRadius(blur) - val opaque = scrimsVisible && !ignoreShadeBlurUntilHidden + if (!blurUtils.supportsBlursOnWindows()) { + blur = 0 + } + + // Brightness slider removes blur, but doesn't affect zooms + blur = (blur * (1f - brightnessMirrorSpring.ratio)).toInt() + + val opaque = scrimsVisible && !blursDisabledForAppLaunch + Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) blurUtils.applyBlur(blurRoot?.viewRootImpl ?: root.viewRootImpl, blur, opaque) - val zoomOut = blurUtils.ratioOfBlurRadius(blur) try { if (root.isAttachedToWindow && root.windowToken != null) { wallpaperManager.setWallpaperZoomOut(root.windowToken, zoomOut) @@ -204,6 +212,7 @@ class NotificationShadeDepthController @Inject constructor( } listeners.forEach { it.onWallpaperZoomOutChanged(zoomOut) + it.onBlurRadiusChanged(blur) } notificationShadeWindowController.setBackgroundBlurRadius(blur) } @@ -259,13 +268,9 @@ class NotificationShadeDepthController @Inject constructor( if (isDozing) { shadeSpring.finishIfRunning() shadeAnimation.finishIfRunning() - globalActionsSpring.finishIfRunning() + brightnessMirrorSpring.finishIfRunning() } } - - override fun onDozeAmountChanged(linear: Float, eased: Float) { - wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased) - } } init { @@ -414,19 +419,15 @@ class NotificationShadeDepthController @Inject constructor( !keyguardStateController.isKeyguardFadingAway } - fun updateGlobalDialogVisibility(visibility: Float, dialogView: View?) { - globalActionsSpring.animateTo(blurUtils.blurRadiusOfRatio(visibility), dialogView) - } - override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { IndentingPrintWriter(pw, " ").let { it.println("StatusBarWindowBlurController:") it.increaseIndent() it.println("shadeRadius: ${shadeSpring.radius}") it.println("shadeAnimation: ${shadeAnimation.radius}") - it.println("globalActionsRadius: ${globalActionsSpring.radius}") + it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}") it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius") - it.println("ignoreShadeBlurUntilHidden: $ignoreShadeBlurUntilHidden") + it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch") } } @@ -511,5 +512,8 @@ class NotificationShadeDepthController @Inject constructor( * Current wallpaper zoom out, where 0 is the closest, and 1 the farthest */ fun onWallpaperZoomOutChanged(zoomOut: Float) + + @JvmDefault + fun onBlurRadiusChanged(blurRadius: Int) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index 045a1976d502..f0d779ce1e0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -182,6 +182,12 @@ public interface NotificationShadeWindowController extends RemoteInputController default void setFaceAuthDisplayBrightness(float brightness) {} /** + * How much {@link LightRevealScrim} obscures the UI. + * @param amount 0 when opaque, 1 when not transparent + */ + default void setLightRevealScrimAmount(float amount) {} + + /** * Custom listener to pipe data back to plugins about whether or not the status bar would be * collapsed if not for the plugin. * TODO: Find cleaner way to do this. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 3c549f94ad0f..396d86bab825 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -158,14 +158,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle final int N = activeNotifications.size(); for (int i = 0; i < N; i++) { NotificationEntry ent = activeNotifications.get(i); - final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent() - && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade( - ent.getKey(), ent.getSbn().getGroupKey()); - if (ent.isRowDismissed() || ent.isRowRemoved() - || isBubbleNotificationSuppressedFromShade - || mFgsSectionController.hasEntry(ent)) { - // we don't want to update removed notifications because they could - // temporarily become children if they were isolated before. + if (shouldSuppressActiveNotification(ent)) { continue; } @@ -254,9 +247,11 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } for (ExpandableNotificationRow viewToRemove : viewsToRemove) { - if (mEntryManager.getPendingOrActiveNotif(viewToRemove.getEntry().getKey()) != null) { + NotificationEntry entry = viewToRemove.getEntry(); + if (mEntryManager.getPendingOrActiveNotif(entry.getKey()) != null + && !shouldSuppressActiveNotification(entry)) { // we are only transferring this notification to its parent, don't generate an - // animation + // animation. If the notification is suppressed, this isn't a transfer. mListContainer.setChildTransferInProgress(true); } if (viewToRemove.isSummaryWithChildren()) { @@ -325,6 +320,23 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle endUpdate(); } + /** + * Should a notification entry from the active list be suppressed and not show? + */ + private boolean shouldSuppressActiveNotification(NotificationEntry ent) { + final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent() + && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade( + ent.getKey(), ent.getSbn().getGroupKey()); + if (ent.isRowDismissed() || ent.isRowRemoved() + || isBubbleNotificationSuppressedFromShade + || mFgsSectionController.hasEntry(ent)) { + // we want to suppress removed notifications because they could + // temporarily become children if they were isolated before. + return true; + } + return false; + } + private void addNotificationChildrenAndSort() { // Let's now add all notification children which are missing boolean orderChanged = false; @@ -423,7 +435,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle final int N = mListContainer.getContainerChildCount(); int visibleNotifications = 0; - boolean onKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; + boolean onKeyguard = + mStatusBarStateController.getCurrentOrUpcomingState() == StatusBarState.KEYGUARD; Stack<ExpandableNotificationRow> stack = new Stack<>(); for (int i = N - 1; i >= 0; i--) { View child = mListContainer.getContainerChildAt(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index 9765ace7179f..b34bfad499f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -179,7 +179,10 @@ constructor( } override fun onTouchEvent(event: MotionEvent): Boolean { - if (!canHandleMotionEvent()) { + val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL || + event.action == MotionEvent.ACTION_UP) && isExpanding + if (!canHandleMotionEvent() && !finishExpanding) { + // We allow cancellations/finishing to still go through here to clean up the state return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java index 924eb263de50..b6aed23e64ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java @@ -132,6 +132,8 @@ public class RemoteInputController { public void removeRemoteInput(NotificationEntry entry, Object token) { Objects.requireNonNull(entry); if (entry.mRemoteEditImeVisible) return; + // If the view is being removed, this may be called even though we're not active + if (!isRemoteInputActive(entry)) return; pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java index dbab8c2dab0e..5c3916c7b41d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java @@ -26,7 +26,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.FeatureFlagUtils; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -61,15 +60,22 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, private int mVisibleState = -1; private DualToneHandler mDualToneHandler; private boolean mForceHidden; - + private boolean mProviderModel; private ImageView mVolte; - public static StatusBarMobileView fromContext(Context context, String slot) { + /** + * Designated constructor + */ + public static StatusBarMobileView fromContext( + Context context, + String slot, + boolean providerModel + ) { LayoutInflater inflater = LayoutInflater.from(context); StatusBarMobileView v = (StatusBarMobileView) inflater.inflate(R.layout.status_bar_mobile_signal_group, null); v.setSlot(slot); - v.init(); + v.init(providerModel); v.setVisibleState(STATE_ICON); return v; } @@ -102,12 +108,13 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, outRect.bottom += translationY; } - private void init() { + private void init(boolean providerModel) { + mProviderModel = providerModel; mDualToneHandler = new DualToneHandler(getContext()); mMobileGroup = findViewById(R.id.mobile_group); mMobile = findViewById(R.id.mobile_signal); mMobileType = findViewById(R.id.mobile_type); - if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlagUtils.SETTINGS_PROVIDER_MODEL)) { + if (mProviderModel) { mMobileRoaming = findViewById(R.id.mobile_roaming_large); } else { mMobileRoaming = findViewById(R.id.mobile_roaming); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index f8a1ff879e72..0725bf961e13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -82,6 +82,7 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll private final UiEventLogger mUiEventLogger; private int mState; private int mLastState; + private int mUpcomingState; private boolean mLeaveOpenOnKeyguardHide; private boolean mKeyguardRequested; @@ -169,6 +170,7 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll } mLastState = mState; mState = state; + mUpcomingState = state; mUiEventLogger.log(StatusBarStateEvent.fromState(mState)); for (RankedListener rl : new ArrayList<>(mListeners)) { rl.mListener.onStateChanged(mState); @@ -184,6 +186,16 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll } @Override + public void setUpcomingState(int nextState) { + mUpcomingState = nextState; + } + + @Override + public int getCurrentOrUpcomingState() { + return mUpcomingState; + } + + @Override public boolean isDozing() { return mIsDozing; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java index 73f3d90bd4f8..25200501a916 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java @@ -74,6 +74,20 @@ public interface SysuiStatusBarStateController extends StatusBarStateController boolean setState(int state, boolean force); /** + * Provides a hint that the status bar has started to transition to another + * {@link StatusBarState}. This suggests that a matching call to setState() with the same value + * will happen in the near future, although that may not happen if the animation is canceled, + * etc. + */ + void setUpcomingState(int state); + + /** + * If the status bar is in the process of transitioning to a new state, returns that state. + * Otherwise, returns the current state. + */ + int getCurrentOrUpcomingState(); + + /** * Update the dozing state from {@link StatusBar}'s perspective * @param isDozing well, are we dozing? * @return {@code true} if the state changed, else {@code false} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt index 3196eba7d33a..4a467ce3c987 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt @@ -39,9 +39,15 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context var rippleInProgress: Boolean = false var radius: Float = 0.0f - set(value) { rippleShader.radius = value } + set(value) { + rippleShader.radius = value + field = value + } var origin: PointF = PointF() - set(value) { rippleShader.origin = value } + set(value) { + rippleShader.origin = value + field = value + } var duration: Long = 1750 init { @@ -94,6 +100,11 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context } override fun onDraw(canvas: Canvas?) { - canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), ripplePaint) + // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover + // the active effect area. Values here should be kept in sync with the + // animation implementation in the ripple shader. + val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * radius * 1.5f + canvas?.drawCircle(origin.x, origin.y, maskRadius, ripplePaint) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 9f82152eb5ed..96b0e7819c7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -51,6 +51,7 @@ import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.util.ArraySet; +import android.view.ContentInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -126,8 +127,12 @@ public final class NotificationEntry extends ListEntry { public int targetSdk; private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; public CharSequence remoteInputText; + // Mimetype and Uri used to display the image in the notification *after* it has been sent. public String remoteInputMimeType; public Uri remoteInputUri; + // ContentInfo used to keep the attachment permission alive until RemoteInput is sent or + // cancelled. + public ContentInfo remoteInputAttachment; private Notification.BubbleMetadata mBubbleMetadata; private ShortcutInfo mShortcutInfo; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 76f9fe728f2f..93166f39ad62 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -2482,7 +2482,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int intrinsicBefore = getIntrinsicHeight(); super.onLayout(changed, left, top, right, bottom); - if (intrinsicBefore != getIntrinsicHeight() && intrinsicBefore != 0) { + if (intrinsicBefore != getIntrinsicHeight() + && (intrinsicBefore != 0 || getActualHeight() > 0)) { notifyHeightChanged(true /* needsAnimation */); } if (mMenuRow != null && mMenuRow.getMenuView() != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java index 8e24890c8812..86c90c7bcb2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java @@ -25,6 +25,7 @@ import android.view.View; import com.android.systemui.R; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; +import com.android.systemui.statusbar.notification.stack.ViewState; public class FooterView extends StackScrollerDecorView { private final int mClearAllTopPadding; @@ -122,6 +123,14 @@ public class FooterView extends StackScrollerDecorView { public boolean hideContent; @Override + public void copyFrom(ViewState viewState) { + super.copyFrom(viewState); + if (viewState instanceof FooterViewState) { + hideContent = ((FooterViewState) viewState).hideContent; + } + } + + @Override public void applyToView(View view) { super.applyToView(view); if (view instanceof FooterView) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 4e6d376919e9..4ad72027b046 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -77,7 +77,6 @@ import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; import com.android.systemui.R; -import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.CommandQueue; @@ -143,7 +142,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable false /* default */); // TODO(b/187291379) disable again before release private static final boolean DEBUG_REMOVE_ANIMATION = SystemProperties.getBoolean( - "persist.debug.nssl.dismiss", true /* default */); + "persist.debug.nssl.dismiss", false /* default */); private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f; private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f; @@ -201,6 +200,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mPaddingBetweenElements; private int mMaxTopPadding; private int mTopPadding; + private boolean mAnimateNextTopPaddingChange; private int mBottomMargin; private int mBottomInset = 0; private float mQsExpansionFraction; @@ -682,8 +682,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable boolean showDismissView = mClearAllEnabled && mController.hasActiveClearableNotifications(ROWS_ALL); RemoteInputController remoteInputController = mRemoteInputManager.getController(); - boolean showFooterView = (showDismissView || mController.hasActiveNotifications()) - && mEmptyShadeView.getVisibility() == GONE + boolean showFooterView = (showDismissView || getVisibleNotificationCount() > 0) && mStatusBarState != StatusBarState.KEYGUARD && !mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying() && (remoteInputController == null || !remoteInputController.isRemoteInputActive()); @@ -1212,16 +1211,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) private void setTopPadding(int topPadding, boolean animate) { if (mTopPadding != topPadding) { + boolean shouldAnimate = animate || mAnimateNextTopPaddingChange; mTopPadding = topPadding; updateAlgorithmHeightAndPadding(); updateContentHeight(); - if (animate && mAnimationsEnabled && mIsExpanded) { + if (shouldAnimate && mAnimationsEnabled && mIsExpanded) { mTopPaddingNeedsAnimation = true; mNeedsAnimation = true; } updateStackPosition(); requestChildrenUpdate(); - notifyHeightChangeListener(null, animate); + notifyHeightChangeListener(null, shouldAnimate); + mAnimateNextTopPaddingChange = false; } } @@ -2071,6 +2072,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable int scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight); int imeInset = getImeInset(); scrollRange += Math.min(imeInset, Math.max(0, contentHeight - (getHeight() - imeInset))); + if (scrollRange > 0) { + scrollRange = Math.max(getScrollAmountToScrollBoundary(), scrollRange); + } return scrollRange; } @@ -3205,6 +3209,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable ignoreChildren); mAnimationEvents.add(event); mSwipedOutViews.remove(child); + if (DEBUG_REMOVE_ANIMATION) { + String key = ""; + if (child instanceof ExpandableNotificationRow) { + key = ((ExpandableNotificationRow) child).getEntry().getKey(); + } + Log.d(TAG, "created Remove Event - SwipedOut: " + childWasSwipedOut + " " + key); + } } mChildrenToRemoveAnimated.clear(); } @@ -5256,6 +5267,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mController.getNoticationRoundessManager() .setViewsAffectedBySwipe(null, null, null, getResources().getBoolean(R.bool.flag_notif_updates)); + // Round bottom corners for notification right before shelf. + mShelf.updateAppearance(); } void setTopHeadsUpEntry(NotificationEntry topEntry) { @@ -5545,6 +5558,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } /** + * Request an animation whenever the toppadding changes next + */ + public void animateNextTopPaddingChange() { + mAnimateNextTopPaddingChange = true; + } + + /** * A listener that is notified when the empty space below the notifications is clicked on */ @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e71f7dbb008b..09afedb6de59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1459,6 +1459,13 @@ public class NotificationStackScrollLayoutController { } /** + * Request an animation whenever the toppadding changes next + */ + public void animateNextTopPaddingChange() { + mView.animateNextTopPaddingChange(); + } + + /** * Enum for UiEvent logged from this class */ enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 8f4a71cf2563..3fc8b8d9aef1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -27,7 +27,6 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.NotificationShelf; -import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -37,9 +36,9 @@ import java.util.ArrayList; import java.util.List; /** - * The Algorithm of the {@link com.android.systemui.statusbar.notification.stack - * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar - * .stack.StackScrollState} + * The Algorithm of the + * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can + * be queried for {@link StackScrollAlgorithmState} */ public class StackScrollAlgorithm { @@ -96,7 +95,7 @@ public class StackScrollAlgorithm { // First we reset the view states to their default values. resetChildViewStates(); - initAlgorithmState(mHostView, algorithmState, ambientState); + initAlgorithmState(algorithmState, ambientState); updatePositionsForState(algorithmState, ambientState); updateZValuesForState(algorithmState, ambientState); updateHeadsUpStates(algorithmState, ambientState); @@ -216,19 +215,18 @@ public class StackScrollAlgorithm { /** * Initialize the algorithm state like updating the visible children. */ - private void initAlgorithmState(ViewGroup hostView, StackScrollAlgorithmState state, - AmbientState ambientState) { + private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) { state.scrollY = ambientState.getScrollY(); state.mCurrentYPosition = -state.scrollY; state.mCurrentExpandedYPosition = -state.scrollY; //now init the visible children and update paddings - int childCount = hostView.getChildCount(); + int childCount = mHostView.getChildCount(); state.visibleChildren.clear(); state.visibleChildren.ensureCapacity(childCount); int notGoneIndex = 0; for (int i = 0; i < childCount; i++) { - ExpandableView v = (ExpandableView) hostView.getChildAt(i); + ExpandableView v = (ExpandableView) mHostView.getChildAt(i); if (v.getVisibility() != View.GONE) { if (v == ambientState.getShelf()) { continue; @@ -237,7 +235,7 @@ public class StackScrollAlgorithm { if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; - // handle the notgoneIndex for the children as well + // handle the notGoneIndex for the children as well List<ExpandableNotificationRow> children = row.getAttachedChildren(); if (row.isSummaryWithChildren() && children != null) { for (ExpandableNotificationRow childRow : children) { @@ -401,34 +399,42 @@ public class StackScrollAlgorithm { if (view instanceof FooterView) { final boolean shadeClosed = !ambientState.isShadeExpanded(); final boolean isShelfShowing = algorithmState.firstViewInShelf != null; - - final float footerEnd = algorithmState.mCurrentExpandedYPosition - + view.getIntrinsicHeight(); - final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); - ((FooterView.FooterViewState) viewState).hideContent = - shadeClosed || isShelfShowing || noSpaceForFooter; - - } else if (view != ambientState.getTrackedHeadsUpRow()) { - if (ambientState.isExpansionChanging()) { - // Show all views. Views below the shelf will later be clipped (essentially hidden) - // in NotificationShelf. - viewState.hidden = false; - viewState.inShelf = algorithmState.firstViewInShelf != null - && i >= algorithmState.visibleChildren.indexOf( - algorithmState.firstViewInShelf); - } else if (ambientState.getShelf() != null) { - // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all - // to shelf start, thereby hiding all notifications (except the first one, which we - // later unhide in updatePulsingState) - final int shelfStart = ambientState.getInnerHeight() - - ambientState.getShelf().getIntrinsicHeight(); - viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); - if (viewState.yTranslation >= shelfStart) { - viewState.hidden = !view.isExpandAnimationRunning() - && !view.hasExpandingChild(); - viewState.inShelf = true; - // Notifications in the shelf cannot be visible HUNs. - viewState.headsUpIsVisible = false; + if (shadeClosed) { + viewState.hidden = true; + } else { + final float footerEnd = algorithmState.mCurrentExpandedYPosition + + view.getIntrinsicHeight(); + final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); + ((FooterView.FooterViewState) viewState).hideContent = + isShelfShowing || noSpaceForFooter; + } + } else { + if (view != ambientState.getTrackedHeadsUpRow()) { + if (ambientState.isExpansionChanging()) { + // Show all views. Views below the shelf will later be clipped (essentially + // hidden) in NotificationShelf. + viewState.hidden = false; + viewState.inShelf = algorithmState.firstViewInShelf != null + && i >= algorithmState.visibleChildren.indexOf( + algorithmState.firstViewInShelf); + } else if (ambientState.getShelf() != null) { + // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all + // to shelf start, thereby hiding all notifications (except the first one, which + // we later unhide in updatePulsingState) + final int stackBottom = + !ambientState.isShadeExpanded() || ambientState.isDozing() + ? ambientState.getInnerHeight() + : (int) ambientState.getStackHeight(); + final int shelfStart = + stackBottom - ambientState.getShelf().getIntrinsicHeight(); + viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); + if (viewState.yTranslation >= shelfStart) { + viewState.hidden = !view.isExpandAnimationRunning() + && !view.hasExpandingChild(); + viewState.inShelf = true; + // Notifications in the shelf cannot be visible HUNs. + viewState.headsUpIsVisible = false; + } } } @@ -484,7 +490,7 @@ public class StackScrollAlgorithm { View previousChild) { return sectionProvider.beginsSection(child, previousChild) && visibleIndex > 0 - && !(previousChild instanceof SilentHeader) + && !(previousChild instanceof SectionHeaderView) && !(child instanceof FooterView); } @@ -695,7 +701,7 @@ public class StackScrollAlgorithm { this.mIsExpanded = isExpanded; } - public class StackScrollAlgorithmState { + public static class StackScrollAlgorithmState { /** * The scroll position of the algorithm (absolute scrolling). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 20e6f60c9b17..917c79fc6de5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -588,7 +588,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp return MODE_UNLOCK_COLLAPSING; } if (mKeyguardViewController.isShowing()) { - if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) { + if ((mKeyguardViewController.bouncerIsOrWillBeShowing() + || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) { if (bypass && mKeyguardBypassController.canPlaySubtleWindowAnimations()) { return MODE_UNLOCK_FADING; } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index 69360b290118..1361acb1e156 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -42,6 +42,7 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; @@ -93,6 +94,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final SystemStatusAnimationScheduler mAnimationScheduler; private final StatusBarLocationPublisher mLocationPublisher; private NotificationIconAreaController mNotificationIconAreaController; + private final FeatureFlags mFeatureFlags; private List<String> mBlockedIcons = new ArrayList<>(); @@ -115,12 +117,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue OngoingCallController ongoingCallController, SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, - NotificationIconAreaController notificationIconAreaController + NotificationIconAreaController notificationIconAreaController, + FeatureFlags featureFlags ) { mOngoingCallController = ongoingCallController; mAnimationScheduler = animationScheduler; mLocationPublisher = locationPublisher; mNotificationIconAreaController = notificationIconAreaController; + mFeatureFlags = featureFlags; } @Override @@ -150,7 +154,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mStatusBar.restoreHierarchyState( savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE)); } - mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons)); + mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons), mFeatureFlags); mDarkIconManager.setShouldLog(true); mBlockedIcons.add(getString(com.android.internal.R.string.status_bar_volume)); mBlockedIcons.add(getString(com.android.internal.R.string.status_bar_alarm_clock)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index 31965d4fc4cd..b4f8126042ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -31,6 +31,7 @@ import com.android.systemui.R; import com.android.systemui.demomode.DemoMode; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarMobileView; import com.android.systemui.statusbar.StatusBarWifiView; @@ -48,16 +49,22 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da private final LinearLayout mStatusIcons; private final ArrayList<StatusBarMobileView> mMobileViews = new ArrayList<>(); private final int mIconSize; + private final FeatureFlags mFeatureFlags; private StatusBarWifiView mWifiView; private boolean mDemoMode; private int mColor; - public DemoStatusIcons(LinearLayout statusIcons, int iconSize) { + public DemoStatusIcons( + LinearLayout statusIcons, + int iconSize, + FeatureFlags featureFlags + ) { super(statusIcons.getContext()); mStatusIcons = statusIcons; mIconSize = iconSize; mColor = DarkIconDispatcher.DEFAULT_ICON_TINT; + mFeatureFlags = featureFlags; if (statusIcons instanceof StatusIconContainer) { setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons()); @@ -247,7 +254,8 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da public void addMobileView(MobileIconState state) { Log.d(TAG, "addMobileView: "); - StatusBarMobileView view = StatusBarMobileView.fromContext(mContext, state.slot); + StatusBarMobileView view = StatusBarMobileView.fromContext( + mContext, state.slot, mFeatureFlags.isCombinedStatusBarSignalIconsEnabled()); view.applyMobileState(state); view.setStaticDrawableColor(mColor); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index c4d1abc1b74c..68024726c5de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -223,8 +223,15 @@ public class DozeParameters implements TunerService.Tunable, * then abruptly showing AOD. */ public boolean shouldControlUnlockedScreenOff() { - return getAlwaysOn() && mFeatureFlags.useNewLockscreenAnimations() - && mUnlockedScreenOffAnimationController.shouldPlayUnlockedScreenOffAnimation(); + return mUnlockedScreenOffAnimationController.shouldPlayUnlockedScreenOffAnimation(); + } + + /** + * Whether we're capable of controlling the screen off animation if we want to. This isn't + * possible if AOD isn't even enabled or if the flag is disabled. + */ + public boolean canControlUnlockedScreenOff() { + return getAlwaysOn() && mFeatureFlags.useNewLockscreenAnimations(); } private boolean getBoolean(String propName, int resId) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index b7f7f5af78a9..077500f7bfb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -459,6 +459,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL mWalletButton.setVisibility(GONE); mIndicationArea.setPadding(0, 0, 0, 0); } else { + Drawable tileIcon = mQuickAccessWalletController.getWalletClient().getTileIcon(); + if (tileIcon != null) { + mWalletButton.setImageDrawable(tileIcon); + } mWalletButton.setVisibility(VISIBLE); mWalletButton.setOnClickListener(this::onWalletClick); mIndicationArea.setPadding(mIndicationPadding, 0, mIndicationPadding, 0); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index b5e550a47022..d6ea4a828395 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -367,6 +367,10 @@ public class KeyguardBouncer { return mShowingSoon || mExpansion != EXPANSION_HIDDEN && mExpansion != EXPANSION_VISIBLE; } + public boolean getShowingSoon() { + return mShowingSoon; + } + /** * @return {@code true} when bouncer's pre-hide animation already started but isn't completely * hidden yet, {@code false} otherwise. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index 26c6fe980ee1..c9d08427c8ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -82,6 +82,7 @@ open class KeyguardBypassController : Dumpable { private set var bouncerShowing: Boolean = false + var altBouncerShowing: Boolean = false var launchingAffordance: Boolean = false var qSExpanded = false set(value) { @@ -172,6 +173,7 @@ open class KeyguardBypassController : Dumpable { if (bypassEnabled) { return when { bouncerShowing -> true + altBouncerShowing -> true statusBarStateController.state != StatusBarState.KEYGUARD -> false launchingAffordance -> false isPulseExpanding || qSExpanded -> false @@ -210,6 +212,7 @@ open class KeyguardBypassController : Dumpable { pw.println(" bypassEnabled: $bypassEnabled") pw.println(" canBypass: ${canBypass()}") pw.println(" bouncerShowing: $bouncerShowing") + pw.println(" altBouncerShowing: $altBouncerShowing") pw.println(" isPulseExpanding: $isPulseExpanding") pw.println(" launchingAffordance: $launchingAffordance") pw.println(" qSExpanded: $qSExpanded") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index fa5011e8802f..ad4213d212a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -234,7 +234,9 @@ public class KeyguardClockPositionAlgorithm { private int getClockY(float panelExpansion, float darkAmount) { float clockYRegular = getExpandedPreferredClockY(); - float clockYBouncer = -mKeyguardStatusHeight; + + // Dividing the height creates a smoother transition when the user swipes up to unlock + float clockYBouncer = -mKeyguardStatusHeight / 3.0f; // Move clock up while collapsing the shade float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java index 96276f46d23d..178974327a75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java @@ -38,7 +38,7 @@ import java.util.LinkedList; * A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open"). */ public class KeyguardIndicationTextView extends TextView { - private static final long MSG_DURATION_MILLIS = 1500; + private static final long MSG_MIN_DURATION_MILLIS_DEFAULT = 1500; private long mNextAnimationTime = 0; private boolean mAnimationsEnabled = true; private LinkedList<CharSequence> mMessages = new LinkedList<>(); @@ -104,8 +104,13 @@ public class KeyguardIndicationTextView extends TextView { long delay = Math.max(0, mNextAnimationTime - timeInMillis); setNextAnimationTime(timeInMillis + delay + getFadeOutDuration()); + final long minDurationMillis = + (indication != null && indication.getMinVisibilityMillis() != null) + ? indication.getMinVisibilityMillis() + : MSG_MIN_DURATION_MILLIS_DEFAULT; + if (!text.equals("") || hasIcon) { - setNextAnimationTime(mNextAnimationTime + MSG_DURATION_MILLIS); + setNextAnimationTime(mNextAnimationTime + minDurationMillis); animSetBuilder.before(getInAnimator()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt index bfe0684b5411..ec2d0364a3a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt @@ -53,7 +53,7 @@ class KeyguardLiftController constructor( // Not listening anymore since trigger events unregister themselves isListening = false updateListeningState() - keyguardUpdateMonitor.requestFaceAuth() + keyguardUpdateMonitor.requestFaceAuth(true) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index eef24200a882..e272d2713e2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -50,6 +50,7 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; @@ -105,6 +106,7 @@ public class KeyguardStatusBarView extends RelativeLayout implements private int mLayoutState = LAYOUT_NONE; private SystemStatusAnimationScheduler mAnimationScheduler; + private FeatureFlags mFeatureFlags; /** * Draw this many pixels into the left/right side of the cutout to optimally use the space @@ -142,6 +144,7 @@ public class KeyguardStatusBarView extends RelativeLayout implements loadBlockList(); mBatteryController = Dependency.get(BatteryController.class); mAnimationScheduler = Dependency.get(SystemStatusAnimationScheduler.class); + mFeatureFlags = Dependency.get(FeatureFlags.class); } @Override @@ -364,7 +367,7 @@ public class KeyguardStatusBarView extends RelativeLayout implements userInfoController.addCallback(this); userInfoController.reloadUserInfo(); Dependency.get(ConfigurationController.class).addCallback(this); - mIconManager = new TintedIconManager(findViewById(R.id.statusIcons)); + mIconManager = new TintedIconManager(findViewById(R.id.statusIcons), mFeatureFlags); mIconManager.setBlockList(mBlockedIcons); Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager); mAnimationScheduler.addCallback(this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index 41af80e02b5a..cfe95e06fb61 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -71,6 +71,7 @@ public class NotificationIconAreaController implements private final DozeParameters mDozeParameters; private final Optional<Bubbles> mBubblesOptional; private final StatusBarWindowController mStatusBarWindowController; + private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private int mIconSize; private int mIconHPadding; @@ -119,7 +120,8 @@ public class NotificationIconAreaController implements Optional<Bubbles> bubblesOptional, DemoModeController demoModeController, DarkIconDispatcher darkIconDispatcher, - StatusBarWindowController statusBarWindowController) { + StatusBarWindowController statusBarWindowController, + UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) { mContrastColorUtil = ContrastColorUtil.getInstance(context); mContext = context; mStatusBarStateController = statusBarStateController; @@ -133,6 +135,7 @@ public class NotificationIconAreaController implements mDemoModeController = demoModeController; mDemoModeController.addCallback(this); mStatusBarWindowController = statusBarWindowController; + mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; notificationListener.addNotificationSettingsListener(mSettingsListener); initializeNotificationAreaViews(context); @@ -677,7 +680,12 @@ public class NotificationIconAreaController implements } boolean visible = mBypassController.getBypassEnabled() || mWakeUpCoordinator.getNotificationsFullyHidden(); - if (mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { + + // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is + // playing, in which case we want them to be visible since we're animating in the AOD UI and + // will be switching to KEYGUARD shortly. + if (mStatusBarStateController.getState() != StatusBarState.KEYGUARD + && !mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) { visible = false; } if (visible && mWakeUpCoordinator.isPulseExpanding()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index b4117604c908..c89f698f2656 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -123,6 +123,7 @@ import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; +import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.PulseExpansionHandler; @@ -182,6 +183,11 @@ public class NotificationPanelViewController extends PanelViewController { private static final boolean DEBUG = false; /** + * The parallax amount of the quick settings translation when dragging down the panel + */ + private static final float QS_PARALLAX_AMOUNT = 0.175f; + + /** * Fling expanding QS. */ private static final int FLING_EXPAND = 0; @@ -316,6 +322,7 @@ public class NotificationPanelViewController extends PanelViewController { private final ScrimController mScrimController; private final PrivacyDotViewController mPrivacyDotViewController; private final QuickAccessWalletController mQuickAccessWalletController; + private final NotificationRemoteInputManager mRemoteInputManager; // Maximum # notifications to show on Keyguard; extras will be collapsed in an overflow card. // If there are exactly 1 + mMaxKeyguardNotifications, then still shows all notifications @@ -377,7 +384,6 @@ public class NotificationPanelViewController extends PanelViewController { private float mLastOverscroll; private boolean mQsExpansionEnabledPolicy = true; private boolean mQsExpansionEnabledAmbient = true; - private boolean mQsExpansionEnabled = mQsExpansionEnabledPolicy && mQsExpansionEnabledAmbient; private ValueAnimator mQsExpansionAnimator; private FlingAnimationUtils mFlingAnimationUtils; private int mStatusBarMinHeight; @@ -548,6 +554,11 @@ public class NotificationPanelViewController extends PanelViewController { private int mDistanceForQSFullShadeTransition; /** + * The translation amount for QS for the full shade transition + */ + private float mQsTranslationForFullShadeTransition; + + /** * The maximum overshoot allowed for the top padding for the full shade transition */ private int mMaxOverscrollAmountForPulse; @@ -562,6 +573,11 @@ public class NotificationPanelViewController extends PanelViewController { private long mNotificationBoundsAnimationDelay; /** + * The duration of the notification bounds animation + */ + private long mNotificationBoundsAnimationDuration; + + /** * Is this a collapse that started on the panel where we should allow the panel to intercept */ private boolean mIsPanelCollapseOnQQS; @@ -588,7 +604,13 @@ public class NotificationPanelViewController extends PanelViewController { * The animator for the qs clipping bounds. */ private ValueAnimator mQsClippingAnimation = null; + + /** + * Is the current animator resetting the qs translation. + */ + private boolean mIsQsTranslationResetAnimator; private final Rect mKeyguardStatusAreaClipBounds = new Rect(); + private final Region mQsInterceptRegion = new Region(); /** * The alpha of the views which only show on the keyguard but not in shade / shade locked @@ -697,7 +719,8 @@ public class NotificationPanelViewController extends PanelViewController { @Main Executor uiExecutor, SecureSettings secureSettings, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, - EmergencyButtonController.Factory emergencyButtonControllerFactory) { + EmergencyButtonController.Factory emergencyButtonControllerFactory, + NotificationRemoteInputManager remoteInputManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, statusBarKeyguardViewManager, latencyTracker, flingAnimationUtilsBuilder.get(), @@ -791,6 +814,8 @@ public class NotificationPanelViewController extends PanelViewController { mAuthController = authController; mLockIconViewController = lockIconViewController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; + mRemoteInputManager = remoteInputManager; + int currentMode = navigationModeController.addListener( mode -> mIsGestureNavigation = QuickStepContract.isGesturalMode(mode)); mIsGestureNavigation = QuickStepContract.isGesturalMode(currentMode); @@ -1335,8 +1360,7 @@ public class NotificationPanelViewController extends PanelViewController { : mNotificationShelfController.getIntrinsicHeight() + notificationPadding; float lockIconPadding = 0; - if (mLockIconViewController.getTop() != 0 - && (mUpdateMonitor.isUdfpsEnrolled() || mUpdateMonitor.isFaceEnrolled())) { + if (mLockIconViewController.getTop() != 0) { lockIconPadding = mStatusBar.getDisplayHeight() - mLockIconViewController.getTop(); } @@ -1466,9 +1490,8 @@ public class NotificationPanelViewController extends PanelViewController { } private void setQsExpansionEnabled() { - mQsExpansionEnabled = mQsExpansionEnabledPolicy && mQsExpansionEnabledAmbient; if (mQs == null) return; - mQs.setHeaderClickable(mQsExpansionEnabled); + mQs.setHeaderClickable(isQsExpansionEnabled()); } public void setQsExpansionEnabledPolicy(boolean qsExpansionEnabledPolicy) { @@ -1537,8 +1560,13 @@ public class NotificationPanelViewController extends PanelViewController { flingSettings(0 /* vel */, animateAway ? FLING_HIDE : FLING_COLLAPSE); } + private boolean isQsExpansionEnabled() { + return mQsExpansionEnabledPolicy && mQsExpansionEnabledAmbient + && !mRemoteInputManager.getController().isRemoteInputActive(); + } + public void expandWithQs() { - if (mQsExpansionEnabled) { + if (isQsExpansionEnabled()) { mQsExpandImmediate = true; mNotificationStackScrollLayoutController.setShouldShowShelfOnly(true); } @@ -1803,7 +1831,7 @@ public class NotificationPanelViewController extends PanelViewController { private boolean handleQsTouch(MotionEvent event) { final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f - && mBarState != KEYGUARD && !mQsExpanded && mQsExpansionEnabled) { + && mBarState != KEYGUARD && !mQsExpanded && isQsExpansionEnabled()) { // Down in the empty area while fully expanded - go to QS. mQsTracking = true; traceQsJank(true /* startTracing */, false /* wasCancelled */); @@ -1825,7 +1853,7 @@ public class NotificationPanelViewController extends PanelViewController { if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mConflictingQsExpansionGesture = false; } - if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed() && mQsExpansionEnabled) { + if (action == MotionEvent.ACTION_DOWN && isFullyCollapsed() && isQsExpansionEnabled()) { mTwoFingerQsExpandPossible = true; } if (mTwoFingerQsExpandPossible && isOpenQsEvent(event) && event.getY(event.getActionIndex()) @@ -2049,7 +2077,7 @@ public class NotificationPanelViewController extends PanelViewController { // When expanding QS, let's authenticate the user if possible, // this will speed up notification actions. if (height == 0) { - mStatusBar.requestFaceAuth(); + mStatusBar.requestFaceAuth(false); } } @@ -2225,7 +2253,8 @@ public class NotificationPanelViewController extends PanelViewController { private void onStackYChanged(boolean shouldAnimate) { if (mQs != null) { if (shouldAnimate) { - mAnimateNextNotificationBounds = true; + animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_STANDARD, + 0 /* delay */); mNotificationBoundsAnimationDelay = 0; } setQSClippingBounds(); @@ -2244,8 +2273,7 @@ public class NotificationPanelViewController extends PanelViewController { private void updateQSExpansionEnabledAmbient() { final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsOffsetHeight; - mQsExpansionEnabledAmbient = - mAmbientState.getScrollY() <= scrollRangeToTop && !mAmbientState.isShadeOpening(); + mQsExpansionEnabledAmbient = mAmbientState.getScrollY() <= scrollRangeToTop; setQsExpansionEnabled(); } @@ -2304,8 +2332,7 @@ public class NotificationPanelViewController extends PanelViewController { final int startBottom = mKeyguardStatusAreaClipBounds.bottom; mQsClippingAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); mQsClippingAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - mQsClippingAnimation.setDuration( - StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); + mQsClippingAnimation.setDuration(mNotificationBoundsAnimationDuration); mQsClippingAnimation.setStartDelay(mNotificationBoundsAnimationDelay); mQsClippingAnimation.addUpdateListener(animation -> { float fraction = animation.getAnimatedFraction(); @@ -2324,6 +2351,7 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void onAnimationEnd(Animator animation) { mQsClippingAnimation = null; + mIsQsTranslationResetAnimator = false; } }); mQsClippingAnimation.start(); @@ -2347,7 +2375,18 @@ public class NotificationPanelViewController extends PanelViewController { statusBarClipTop = top - mKeyguardStatusBar.getTop(); } if (mQs != null) { - mQs.setFancyClipping(top, bottom, radius, qsVisible + float qsTranslation = 0; + if (mTransitioningToFullShadeProgress > 0.0f || (mQsClippingAnimation != null + && mIsQsTranslationResetAnimator)) { + qsTranslation = (top - mQs.getHeader().getHeight()) * QS_PARALLAX_AMOUNT; + } + mQsTranslationForFullShadeTransition = qsTranslation; + updateQsFrameTranslation(); + float currentTranslation = mQsFrame.getTranslationY(); + mQs.setFancyClipping(( + int) (top - currentTranslation), + (int) (bottom - currentTranslation), + radius, qsVisible && !mShouldUseSplitNotificationShade); } mKeyguardStatusViewController.setClipBounds( @@ -2481,8 +2520,11 @@ public class NotificationPanelViewController extends PanelViewController { * shade. 0.0f means we're not transitioning yet. */ public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) { - mAnimateNextNotificationBounds = animate && !mShouldUseSplitNotificationShade; - mNotificationBoundsAnimationDelay = delay; + if (animate && !mShouldUseSplitNotificationShade) { + animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, + delay); + mIsQsTranslationResetAnimator = mQsTranslationForFullShadeTransition > 0.0f; + } float endPosition = 0; if (pxAmount > 0.0f) { @@ -2661,15 +2703,21 @@ public class NotificationPanelViewController extends PanelViewController { * @return Whether we should intercept a gesture to open Quick Settings. */ private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { - if (!mQsExpansionEnabled || mCollapsedOnDown || (mKeyguardShowing + if (!isQsExpansionEnabled() || mCollapsedOnDown || (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled())) { return false; } View header = mKeyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader(); - final boolean - onHeader = - x >= mQsFrame.getX() && x <= mQsFrame.getX() + mQsFrame.getWidth() - && y >= header.getTop() && y <= header.getBottom(); + + mQsInterceptRegion.set( + /* left= */ (int) mQsFrame.getX(), + /* top= */ header.getTop(), + /* right= */ (int) mQsFrame.getX() + mQsFrame.getWidth(), + /* bottom= */ header.getBottom()); + // Also allow QS to intercept if the touch is near the notch. + mStatusBarTouchableRegionManager.updateRegionForNotch(mQsInterceptRegion); + final boolean onHeader = mQsInterceptRegion.contains((int) x, (int) y); + if (mQsExpanded) { return onHeader || (yDiff < 0 && isInQsArea(x, y)); } else { @@ -2784,7 +2832,6 @@ public class NotificationPanelViewController extends PanelViewController { private int calculatePanelHeightShade() { int emptyBottomMargin = mNotificationStackScrollLayoutController.getEmptyBottomMargin(); int maxHeight = mNotificationStackScrollLayoutController.getHeight() - emptyBottomMargin; - maxHeight += mNotificationStackScrollLayoutController.getTopPaddingOverflow(); if (mBarState == KEYGUARD) { int minKeyguardPanelBottom = mClockPositionAlgorithm.getLockscreenStatusViewHeight() @@ -2876,7 +2923,7 @@ public class NotificationPanelViewController extends PanelViewController { float startHeight = -mQsExpansionHeight; if (!mShouldUseSplitNotificationShade && mBarState == StatusBarState.SHADE) { // Small parallax as we pull down and clip QS - startHeight = -mQsExpansionHeight * 0.2f; + startHeight = -mQsExpansionHeight * QS_PARALLAX_AMOUNT; } if (mKeyguardBypassController.getBypassEnabled() && isOnKeyguard()) { if (mNotificationStackScrollLayoutController.isPulseExpanding()) { @@ -3058,10 +3105,15 @@ public class NotificationPanelViewController extends PanelViewController { super.setOverExpansion(overExpansion); // Translating the quick settings by half the overexpansion to center it in the background // frame - mQsFrame.setTranslationY(overExpansion / 2f); + updateQsFrameTranslation(); mNotificationStackScrollLayoutController.setOverExpansion(overExpansion); } + private void updateQsFrameTranslation() { + float translation = mOverExpansion / 2.0f + mQsTranslationForFullShadeTransition; + mQsFrame.setTranslationY(translation); + } + @Override protected void onTrackingStarted() { mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen()); @@ -3187,7 +3239,7 @@ public class NotificationPanelViewController extends PanelViewController { case KEYGUARD: if (!mDozingOnDown) { if (mKeyguardBypassController.getBypassEnabled()) { - mUpdateMonitor.requestFaceAuth(); + mUpdateMonitor.requestFaceAuth(true); } else { mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); @@ -3474,14 +3526,21 @@ public class NotificationPanelViewController extends PanelViewController { return !isFullWidth() || !mShowIconsWhenExpanded; } + public final QS.ScrollListener mScrollListener = scrollY -> { + if (scrollY > 0 && !mQsFullyExpanded) { + if (DEBUG) Log.d(TAG, "Scrolling while not expanded. Forcing expand"); + // If we are scrolling QS, we should be fully expanded. + expandWithQs(); + } + }; + private final FragmentListener mFragmentListener = new FragmentListener() { @Override public void onFragmentViewCreated(String tag, Fragment fragment) { mQs = (QS) fragment; mQs.setPanelView(mHeightListener); mQs.setExpandClickListener(mOnClickListener); - mQs.setHeaderClickable(mQsExpansionEnabled); - mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade); + mQs.setHeaderClickable(isQsExpansionEnabled()); updateQSPulseExpansion(); mQs.setOverscrolling(mStackScrollerOverscrolling); mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade); @@ -3495,8 +3554,16 @@ public class NotificationPanelViewController extends PanelViewController { mHeightListener.onQsHeightChanged(); } }); + mQs.setCollapsedMediaVisibilityChangedListener((visible) -> { + if (mQs.getHeader().isShown()) { + animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_STANDARD, + 0 /* delay */); + mNotificationStackScrollLayoutController.animateNextTopPaddingChange(); + } + }); mLockscreenShadeTransitionController.setQS(mQs); mNotificationStackScrollLayoutController.setQsContainer((ViewGroup) mQs.getView()); + mQs.setScrollListener(mScrollListener); updateQsExpansion(); } @@ -3511,6 +3578,12 @@ public class NotificationPanelViewController extends PanelViewController { } }; + private void animateNextNotificationBounds(long duration, long delay) { + mAnimateNextNotificationBounds = true; + mNotificationBoundsAnimationDuration = duration; + mNotificationBoundsAnimationDelay = delay; + } + @Override public void setTouchAndAnimationDisabled(boolean disabled) { super.setTouchAndAnimationDisabled(disabled); @@ -3832,8 +3905,13 @@ public class NotificationPanelViewController extends PanelViewController { expand(true /* animate */); } initDownStates(event); - if (!mIsExpanding && !shouldQuickSettingsIntercept(mDownX, mDownY, 0) - && mPulseExpansionHandler.onTouchEvent(event)) { + + // If pulse is expanding already, let's give it the touch. There are situations + // where the panel starts expanding even though we're also pulsing + boolean pulseShouldGetTouch = (!mIsExpanding + && !shouldQuickSettingsIntercept(mDownX, mDownY, 0)) + || mPulseExpansionHandler.isExpanding(); + if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { // We're expanding all the other ones shouldn't get this anymore return true; } @@ -3865,6 +3943,10 @@ public class NotificationPanelViewController extends PanelViewController { mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); } + if (mLockIconViewController.onTouchEvent(event)) { + return true; + } + handled |= super.onTouch(v, event); return !mDozing || mPulsing || handled; } @@ -3954,7 +4036,7 @@ public class NotificationPanelViewController extends PanelViewController { if (mQsExpanded) { flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */, true /* isClick */); - } else if (mQsExpansionEnabled) { + } else if (isQsExpansionEnabled()) { mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0); flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */, true /* isClick */); @@ -3971,7 +4053,7 @@ public class NotificationPanelViewController extends PanelViewController { return; } cancelQsAnimation(); - if (!mQsExpansionEnabled) { + if (!isQsExpansionEnabled()) { amount = 0f; } float rounded = amount >= 1f ? amount : 0f; @@ -3999,8 +4081,9 @@ public class NotificationPanelViewController extends PanelViewController { setOverScrolling(false); } setQsExpansion(mQsExpansionHeight); - flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, - open && mQsExpansionEnabled ? FLING_EXPAND : FLING_COLLAPSE, () -> { + boolean canExpand = isQsExpansionEnabled(); + flingSettings(!canExpand && open ? 0f : velocity, + open && canExpand ? FLING_EXPAND : FLING_COLLAPSE, () -> { setOverScrolling(false); updateQsState(); }, false /* isClick */); @@ -4362,6 +4445,8 @@ public class NotificationPanelViewController extends PanelViewController { */ public void showAodUi() { setDozing(true /* dozing */, false /* animate */, null); + mStatusBarStateController.setUpcomingState(KEYGUARD); + mEntryManager.updateNotifications("showAodUi"); mStatusBarStateListener.onStateChanged(KEYGUARD); mStatusBarStateListener.onDozeAmountChanged(1f, 1f); setExpandedFraction(1f); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java index c95879650049..022faf78b946 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java @@ -38,8 +38,10 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; import com.android.systemui.R; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; @@ -83,9 +85,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final LayoutParams mLpChanged; private final boolean mKeyguardScreenRotation; private final long mLockScreenDisplayTimeout; - private final float mKeyguardRefreshRate; + private final float mKeyguardPreferredRefreshRate; // takes precedence over max + private final float mKeyguardMaxRefreshRate; private final KeyguardViewMediator mKeyguardViewMediator; private final KeyguardBypassController mKeyguardBypassController; + private final AuthController mAuthController; private ViewGroup mNotificationShadeView; private LayoutParams mLp; private boolean mHasTopUi; @@ -99,6 +103,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mCallbacks = Lists.newArrayList(); private final SysuiColorExtractor mColorExtractor; + private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private float mFaceAuthDisplayBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; @Inject @@ -110,7 +115,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW KeyguardBypassController keyguardBypassController, SysuiColorExtractor colorExtractor, DumpManager dumpManager, - KeyguardStateController keyguardStateController) { + KeyguardStateController keyguardStateController, + UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, + AuthController authController) { mContext = context; mWindowManager = windowManager; mActivityManager = activityManager; @@ -121,7 +128,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mKeyguardViewMediator = keyguardViewMediator; mKeyguardBypassController = keyguardBypassController; mColorExtractor = colorExtractor; + mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; dumpManager.registerDumpable(getClass().getName(), this); + mAuthController = authController; mLockScreenDisplayTimeout = context.getResources() .getInteger(R.integer.config_lockScreenDisplayTimeout); @@ -130,13 +139,25 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER); configurationController.addCallback(this); - Display.Mode[] supportedModes = context.getDisplay().getSupportedModes(); - Display.Mode currentMode = context.getDisplay().getMode(); + float desiredPreferredRefreshRate = context.getResources() + .getInteger(R.integer.config_keyguardRefreshRate); + float actualPreferredRefreshRate = -1; + if (desiredPreferredRefreshRate > -1) { + for (Display.Mode displayMode : context.getDisplay().getSupportedModes()) { + if (Math.abs(displayMode.getRefreshRate() - desiredPreferredRefreshRate) <= .1) { + actualPreferredRefreshRate = displayMode.getRefreshRate(); + break; + } + } + } + + mKeyguardPreferredRefreshRate = actualPreferredRefreshRate; + // Running on the highest frame rate available can be expensive. // Let's specify a preferred refresh rate, and allow higher FPS only when we // know that we're not falsing (because we unlocked.) - mKeyguardRefreshRate = context.getResources() - .getInteger(R.integer.config_keyguardRefreshRate); + mKeyguardMaxRefreshRate = context.getResources() + .getInteger(R.integer.config_keyguardMaxRefreshRate); } /** @@ -251,7 +272,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private void applyKeyguardFlags(State state) { final boolean scrimsOccludingWallpaper = - state.mScrimsVisibility == ScrimController.OPAQUE; + state.mScrimsVisibility == ScrimController.OPAQUE || state.mLightRevealScrimOpaque; final boolean keyguardOrAod = state.mKeyguardShowing || (state.mDozing && mDozeParameters.getAlwaysOn()); if ((keyguardOrAod && !state.mBackdropShowing && !scrimsOccludingWallpaper) @@ -271,12 +292,26 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mLpChanged.privateFlags &= ~LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; } - if (mKeyguardRefreshRate > 0) { + if (mKeyguardPreferredRefreshRate > 0) { + boolean onKeyguard = state.mStatusBarState == StatusBarState.KEYGUARD + && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway + && !state.mDozing; + if (onKeyguard + && mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) { + mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate; + mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate; + } else { + mLpChanged.preferredMaxDisplayRefreshRate = 0; + mLpChanged.preferredMinDisplayRefreshRate = 0; + } + Trace.setCounter("display_set_preferred_refresh_rate", + (long) mKeyguardPreferredRefreshRate); + } else if (mKeyguardMaxRefreshRate > 0) { boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled() && state.mStatusBarState == StatusBarState.KEYGUARD && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway; if (state.mDozing || bypassOnKeyguard) { - mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardRefreshRate; + mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardMaxRefreshRate; } else { mLpChanged.preferredMaxDisplayRefreshRate = 0; } @@ -300,7 +335,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private void applyFocusableFlag(State state) { boolean panelFocusable = state.mNotificationShadeFocusable && state.mPanelExpanded; if (state.mBouncerShowing && (state.mKeyguardOccluded || state.mKeyguardNeedsInput) - || ENABLE_REMOTE_INPUT && state.mRemoteInputActive) { + || ENABLE_REMOTE_INPUT && state.mRemoteInputActive + // Make the panel focusable if we're doing the screen off animation, since the light + // reveal scrim is drawing in the panel and should consume touch events so that they + // don't go to the app behind. + || mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) { @@ -563,6 +602,16 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } @Override + public void setLightRevealScrimAmount(float amount) { + boolean lightRevealScrimOpaque = amount == 0; + if (mCurrentState.mLightRevealScrimOpaque == lightRevealScrimOpaque) { + return; + } + mCurrentState.mLightRevealScrimOpaque = lightRevealScrimOpaque; + apply(mCurrentState); + } + + @Override public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) { mCurrentState.mWallpaperSupportsAmbientMode = supportsAmbientMode; apply(mCurrentState); @@ -668,7 +717,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(TAG + ":"); - pw.println(" mKeyguardRefreshRate=" + mKeyguardRefreshRate); + pw.println(" mKeyguardMaxRefreshRate=" + mKeyguardMaxRefreshRate); + pw.println(" mKeyguardPreferredRefreshRate=" + mKeyguardPreferredRefreshRate); pw.println(mCurrentState); if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) { mNotificationShadeView.getViewRootImpl().dump(" ", pw); @@ -727,6 +777,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW boolean mKeyguardGoingAway; boolean mQsExpanded; boolean mHeadsUpShowing; + boolean mLightRevealScrimOpaque; boolean mForceCollapsed; boolean mForceDozeBrightness; int mFaceAuthDisplayBrightness; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java index 7f4dabd3f59f..b5d9bd67bd2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java @@ -26,6 +26,7 @@ import android.media.session.MediaSessionLegacyHelper; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.util.Log; import android.view.GestureDetector; import android.view.InputDevice; import android.view.KeyEvent; @@ -66,6 +67,7 @@ import javax.inject.Inject; * Controller for {@link NotificationShadeWindowView}. */ public class NotificationShadeWindowViewController { + private static final String TAG = "NotifShadeWindowVC"; private final InjectionInflationController mInjectionInflationController; private final NotificationWakeUpCoordinator mCoordinator; private final PulseExpansionHandler mPulseExpansionHandler; @@ -213,6 +215,10 @@ public class NotificationShadeWindowViewController { mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() { @Override public Boolean handleDispatchTouchEvent(MotionEvent ev) { + if (mStatusBarView == null) { + Log.w(TAG, "Ignoring touch while statusBarView not yet set."); + return false; + } boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN; boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP; boolean isCancel = ev.getActionMasked() == MotionEvent.ACTION_CANCEL; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index c9d1083135ab..8eb22c63bbcf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1185,7 +1185,8 @@ public class StatusBar extends SystemUI implements DemoMode, mOngoingCallController, mAnimationScheduler, mStatusBarLocationPublisher, - mNotificationIconAreaController), + mNotificationIconAreaController, + mFeatureFlags), CollapsedStatusBarFragment.TAG) .commit(); @@ -1243,6 +1244,8 @@ public class StatusBar extends SystemUI implements DemoMode, mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront, scrimForBubble); mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim); + mLightRevealScrim.setRevealAmountListener( + mNotificationShadeWindowController::setLightRevealScrimAmount); mUnlockedScreenOffAnimationController.initialize(this, mLightRevealScrim); updateLightRevealScrimVisibility(); @@ -1723,9 +1726,9 @@ public class StatusBar extends SystemUI implements DemoMode, /** * Asks {@link KeyguardUpdateMonitor} to run face auth. */ - public void requestFaceAuth() { + public void requestFaceAuth(boolean userInitiatedRequest) { if (!mKeyguardStateController.canDismissLockScreen()) { - mKeyguardUpdateMonitor.requestFaceAuth(); + mKeyguardUpdateMonitor.requestFaceAuth(userInitiatedRequest); } } @@ -2112,8 +2115,8 @@ public class StatusBar extends SystemUI implements DemoMode, } @Override - public void disableKeyguardBlurs() { - mMainThreadHandler.post(mKeyguardViewMediator::disableBlursUntilHidden); + public void setBlursDisabledForAppLaunch(boolean disabled) { + mKeyguardViewMediator.setBlursDisabledForAppLaunch(disabled); } public boolean isDeviceInVrMode() { @@ -3894,15 +3897,6 @@ public class StatusBar extends SystemUI implements DemoMode, updateQsExpansionEnabled(); mKeyguardViewMediator.setDozing(mDozing); - if ((isDozing && mWakefulnessLifecycle.getLastSleepReason() - == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) - || (!isDozing && mWakefulnessLifecycle.getLastWakeReason() - == PowerManager.WAKE_REASON_POWER_BUTTON)) { - mLightRevealScrim.setRevealEffect(mPowerButtonReveal); - } else if (!(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { - mLightRevealScrim.setRevealEffect(LiftReveal.INSTANCE); - } - mNotificationsController.requestNotificationUpdate("onDozingChanged"); updateDozingState(); mDozeServiceHost.updateDozing(); @@ -3911,6 +3905,27 @@ public class StatusBar extends SystemUI implements DemoMode, Trace.endSection(); } + /** + * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example, + * from the power button). + * @param wakingUp Whether we're updating because we're waking up (true) or going to sleep + * (false). + */ + private void updateRevealEffect(boolean wakingUp) { + if (mLightRevealScrim == null) { + return; + } + + if (wakingUp && mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON + || !wakingUp && mWakefulnessLifecycle.getLastSleepReason() + == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) { + mLightRevealScrim.setRevealEffect(mPowerButtonReveal); + } else if (!(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { + mLightRevealScrim.setRevealEffect(LiftReveal.INSTANCE); + } + } + public LightRevealScrim getLightRevealScrim() { return mLightRevealScrim; } @@ -4043,6 +4058,7 @@ public class StatusBar extends SystemUI implements DemoMode, public void onStartedGoingToSleep() { String tag = "StatusBar#onStartedGoingToSleep"; DejankUtils.startDetectingBlockingIpcs(tag); + updateRevealEffect(false /* wakingUp */); updateNotificationPanelTouchState(); notifyHeadsUpGoingToSleep(); dismissVolumeDialog(); @@ -4074,6 +4090,7 @@ public class StatusBar extends SystemUI implements DemoMode, // This is intentionally below the stopDozing call above, since it avoids that we're // unnecessarily animating the wakeUp transition. Animations should only be enabled // once we fully woke up. + updateRevealEffect(true /* wakingUp */); updateNotificationPanelTouchState(); mPulseExpansionHandler.onStartedWakingUp(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 93b83d3cbcbd..2c7553487067 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -38,6 +38,7 @@ import com.android.systemui.R; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarMobileView; import com.android.systemui.statusbar.StatusBarWifiView; @@ -121,8 +122,8 @@ public interface StatusBarIconController { private final DarkIconDispatcher mDarkIconDispatcher; private int mIconHPadding; - public DarkIconManager(LinearLayout linearLayout) { - super(linearLayout); + public DarkIconManager(LinearLayout linearLayout, FeatureFlags featureFlags) { + super(linearLayout, featureFlags); mIconHPadding = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_icon_padding); mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class); @@ -182,8 +183,8 @@ public interface StatusBarIconController { class TintedIconManager extends IconManager { private int mColor; - public TintedIconManager(ViewGroup group) { - super(group); + public TintedIconManager(ViewGroup group, FeatureFlags featureFlags) { + super(group, featureFlags); } @Override @@ -218,6 +219,7 @@ public interface StatusBarIconController { * Turns info from StatusBarIconController into ImageViews in a ViewGroup. */ class IconManager implements DemoModeCommandReceiver { + private final FeatureFlags mFeatureFlags; protected final ViewGroup mGroup; protected final Context mContext; protected final int mIconSize; @@ -231,7 +233,8 @@ public interface StatusBarIconController { protected ArrayList<String> mBlockList = new ArrayList<>(); - public IconManager(ViewGroup group) { + public IconManager(ViewGroup group, FeatureFlags featureFlags) { + mFeatureFlags = featureFlags; mGroup = group; mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( @@ -332,7 +335,8 @@ public interface StatusBarIconController { } private StatusBarMobileView onCreateStatusBarMobileView(String slot) { - StatusBarMobileView view = StatusBarMobileView.fromContext(mContext, slot); + StatusBarMobileView view = StatusBarMobileView.fromContext( + mContext, slot, mFeatureFlags.isCombinedStatusBarSignalIconsEnabled()); return view; } @@ -452,7 +456,7 @@ public interface StatusBarIconController { } protected DemoStatusIcons createDemoStatusIcons() { - return new DemoStatusIcons((LinearLayout) mGroup, mIconSize); + return new DemoStatusIcons((LinearLayout) mGroup, mIconSize, mFeatureFlags); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index e8463992ed13..6f63b1780d68 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -42,6 +42,8 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardMessageArea; +import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.KeyguardViewController; @@ -50,6 +52,7 @@ import com.android.systemui.DejankUtils; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.FaceAuthScreenBrightnessController; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.system.QuickStepContract; @@ -80,7 +83,7 @@ import javax.inject.Inject; public class StatusBarKeyguardViewManager implements RemoteInputController.Callback, StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener, PanelExpansionListener, NavigationModeController.ModeChangedListener, - KeyguardViewController { + KeyguardViewController, WakefulnessLifecycle.Observer { // When hiding the Keyguard with timing supplied from WindowManager, better be early than late. private static final long HIDE_TIMING_CORRECTION_MS = - 16 * 3; @@ -104,6 +107,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final NotificationShadeWindowController mNotificationShadeWindowController; private final Optional<FaceAuthScreenBrightnessController> mFaceAuthScreenBrightnessController; private final KeyguardBouncer.Factory mKeyguardBouncerFactory; + private final WakefulnessLifecycle mWakefulnessLifecycle; + private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; + private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; + private KeyguardMessageAreaController mKeyguardMessageAreaController; private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() { @Override public void onFullyShown() { @@ -189,6 +196,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private boolean mLastPulsing; private int mLastBiometricMode; private boolean mQsExpanded; + private boolean mAnimatedToSleep; private OnDismissAction mAfterKeyguardGoneAction; private Runnable mKeyguardGoneCancelAction; @@ -232,7 +240,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb KeyguardStateController keyguardStateController, Optional<FaceAuthScreenBrightnessController> faceAuthScreenBrightnessController, NotificationMediaManager notificationMediaManager, - KeyguardBouncer.Factory keyguardBouncerFactory) { + KeyguardBouncer.Factory keyguardBouncerFactory, + WakefulnessLifecycle wakefulnessLifecycle, + UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, + KeyguardMessageAreaController.Factory keyguardMessageAreaFactory) { mContext = context; mViewMediatorCallback = callback; mLockPatternUtils = lockPatternUtils; @@ -246,6 +257,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mDockManager = dockManager; mFaceAuthScreenBrightnessController = faceAuthScreenBrightnessController; mKeyguardBouncerFactory = keyguardBouncerFactory; + mWakefulnessLifecycle = wakefulnessLifecycle; + mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; + mKeyguardMessageAreaFactory = keyguardMessageAreaFactory; } @Override @@ -263,6 +277,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb notificationPanelViewController.addExpansionListener(this); mBypassController = bypassController; mNotificationContainer = notificationContainer; + mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create( + KeyguardMessageArea.findSecurityMessageDisplay(container)); mFaceAuthScreenBrightnessController.ifPresent((it) -> { View overlay = new View(mContext); container.addView(overlay); @@ -301,6 +317,20 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mDockManager.addListener(mDockEventListener); mIsDocked = mDockManager.isDocked(); } + mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() { + @Override + public void onFinishedWakingUp() { + mAnimatedToSleep = false; + updateStates(); + } + + @Override + public void onFinishedGoingToSleep() { + mAnimatedToSleep = + mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying(); + updateStates(); + } + }); } @Override @@ -390,9 +420,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb */ public void showGenericBouncer(boolean scrimmed) { if (mAlternateAuthInterceptor != null) { - if (mAlternateAuthInterceptor.showAlternateAuthBouncer()) { - mStatusBar.updateScrimController(); - } + updateAlternateAuthShowing(mAlternateAuthInterceptor.showAlternateAuthBouncer()); return; } @@ -459,9 +487,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = null; } - if (mAlternateAuthInterceptor.showAlternateAuthBouncer()) { - mStatusBar.updateScrimController(); - } + updateAlternateAuthShowing(mAlternateAuthInterceptor.showAlternateAuthBouncer()); return; } @@ -495,6 +521,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void reset(boolean hideBouncerWhenShowing) { if (mShowing) { + mNotificationPanelViewController.closeQs(); if (mOccluded && !mDozing) { mStatusBar.hideKeyguard(); if (hideBouncerWhenShowing || mBouncer.needsFullscreenBouncer()) { @@ -513,9 +540,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * Stop showing any alternate auth methods */ public void resetAlternateAuth(boolean forceUpdateScrim) { - if ((mAlternateAuthInterceptor != null + final boolean updateScrim = (mAlternateAuthInterceptor != null && mAlternateAuthInterceptor.hideAlternateAuthBouncer()) - || forceUpdateScrim) { + || forceUpdateScrim; + updateAlternateAuthShowing(updateScrim); + } + + private void updateAlternateAuthShowing(boolean updateScrim) { + if (mKeyguardMessageAreaController != null) { + mKeyguardMessageAreaController.setAltBouncerShowing(isShowingAlternateAuth()); + } + mBypassController.setAltBouncerShowing(isShowingAlternateAuth()); + + if (updateScrim) { mStatusBar.updateScrimController(); } } @@ -852,12 +889,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public boolean isBouncerShowing() { - return mBouncer.isShowing(); + return mBouncer.isShowing() || isShowingAlternateAuth(); } @Override public boolean bouncerIsOrWillBeShowing() { - return mBouncer.isShowing() || mBouncer.inTransit(); + return mBouncer.isShowing() || mBouncer.getShowingSoon(); } public boolean isFullscreenBouncer() { @@ -981,7 +1018,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb boolean hideWhileDozing = mDozing && biometricMode != MODE_WAKE_AND_UNLOCK_PULSING; boolean keyguardWithGestureNav = (keyguardShowing && !mDozing || mPulsing && !mIsDocked) && mGesturalNav; - return (!keyguardShowing && !hideWhileDozing || mBouncer.isShowing() + return (!mAnimatedToSleep && !keyguardShowing && !hideWhileDozing || mBouncer.isShowing() || mRemoteInputActive || keyguardWithGestureNav || mGlobalActionsVisible); } @@ -1066,7 +1103,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public void showBouncerMessage(String message, ColorStateList colorState) { - mBouncer.showMessage(message, colorState); + if (isShowingAlternateAuth()) { + if (mKeyguardMessageAreaController != null) { + mKeyguardMessageAreaController.setNextMessageColor(colorState); + mKeyguardMessageAreaController.setMessage(message); + } + } else { + mBouncer.showMessage(message, colorState); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 983b296e006b..95712cd303f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -27,7 +27,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; -import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.view.View; @@ -35,6 +34,7 @@ import android.view.ViewParent; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.ActionClickLogger; @@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.concurrent.Executor; + import javax.inject.Inject; /** @@ -65,6 +67,7 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, private final Context mContext; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final ShadeController mShadeController; + private Executor mExecutor; private final ActivityIntentHelper mActivityIntentHelper; private final GroupExpansionManager mGroupExpansionManager; private View mPendingWorkRemoteInputView; @@ -74,7 +77,6 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, private final ActionClickLogger mActionClickLogger; private int mDisabled2; protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver(); - private Handler mMainHandler = new Handler(); /** */ @@ -89,10 +91,12 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, ActivityStarter activityStarter, ShadeController shadeController, CommandQueue commandQueue, - ActionClickLogger clickLogger) { + ActionClickLogger clickLogger, + @Main Executor executor) { mContext = context; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mShadeController = shadeController; + mExecutor = executor; mContext.registerReceiverAsUser(mChallengeReceiver, UserHandle.ALL, new IntentFilter(ACTION_DEVICE_LOCKED_CHANGED), null, null); mLockscreenUserManager = notificationLockscreenUserManager; @@ -113,9 +117,10 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, boolean hasPendingRemoteInput = mPendingRemoteInputView != null; if (state == StatusBarState.SHADE && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) { - if (!mStatusBarStateController.isKeyguardRequested()) { + if (!mStatusBarStateController.isKeyguardRequested() + && mKeyguardStateController.isUnlocked()) { if (hasPendingRemoteInput) { - mMainHandler.post(mPendingRemoteInputView::callOnClick); + mExecutor.execute(mPendingRemoteInputView::callOnClick); } mPendingRemoteInputView = null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java index ae619462eae0..19ae79abf8e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java @@ -25,6 +25,7 @@ import android.util.Log; import com.android.settingslib.mobile.TelephonyIcons; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators; @@ -63,6 +64,7 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba private final Handler mHandler = Handler.getMain(); private final CarrierConfigTracker mCarrierConfigTracker; private final TunerService mTunerService; + private final FeatureFlags mFeatureFlags; private boolean mHideAirplane; private boolean mHideMobile; @@ -82,9 +84,15 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba private WifiIconState mWifiIconState = new WifiIconState(); @Inject - public StatusBarSignalPolicy(Context context, StatusBarIconController iconController, - CarrierConfigTracker carrierConfigTracker, NetworkController networkController, - SecurityController securityController, TunerService tunerService) { + public StatusBarSignalPolicy( + Context context, + StatusBarIconController iconController, + CarrierConfigTracker carrierConfigTracker, + NetworkController networkController, + SecurityController securityController, + TunerService tunerService, + FeatureFlags featureFlags + ) { mContext = context; mIconController = iconController; @@ -92,6 +100,7 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba mNetworkController = networkController; mSecurityController = securityController; mTunerService = tunerService; + mFeatureFlags = featureFlags; mSlotAirplane = mContext.getString(com.android.internal.R.string.status_bar_airplane); mSlotMobile = mContext.getString(com.android.internal.R.string.status_bar_mobile); @@ -366,6 +375,9 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba @Override public void setConnectivityStatus(boolean noDefaultNetwork, boolean noValidatedNetwork, boolean noNetworksAvailable) { + if (!mFeatureFlags.isCombinedStatusBarSignalIconsEnabled()) { + return; + } if (DEBUG) { Log.d(TAG, "setConnectivityStatus: " + "noDefaultNetwork = " + noDefaultNetwork + "," diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java index b859250a2442..d3d90639546a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java @@ -223,7 +223,7 @@ public final class StatusBarTouchableRegionManager implements Dumpable { } } - private void updateRegionForNotch(Region touchableRegion) { + void updateRegionForNotch(Region touchableRegion) { WindowInsets windowInsets = mNotificationShadeWindowView.getRootWindowInsets(); if (windowInsets == null) { Log.w(TAG, "StatusBarWindowView is not attached."); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 9a04d39c4e9e..4167287a504e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -45,7 +45,8 @@ class UnlockedScreenOffAnimationController @Inject constructor( private val wakefulnessLifecycle: WakefulnessLifecycle, private val statusBarStateControllerImpl: StatusBarStateControllerImpl, private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>, - private val keyguardStateController: KeyguardStateController + private val keyguardStateController: KeyguardStateController, + private val dozeParameters: dagger.Lazy<DozeParameters> ) : WakefulnessLifecycle.Observer { private val handler = Handler() @@ -55,9 +56,16 @@ class UnlockedScreenOffAnimationController @Inject constructor( private var lightRevealAnimationPlaying = false private var aodUiAnimationPlaying = false + /** + * The result of our decision whether to play the screen off animation in + * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to + * sleep. + */ + private var decidedToAnimateGoingToSleep: Boolean? = null + private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { duration = LIGHT_REVEAL_ANIMATION_DURATION - interpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE + interpolator = Interpolators.LINEAR addUpdateListener { lightRevealScrim.revealAmount = it.animatedValue as Float } addListener(object : AnimatorListenerAdapter() { override fun onAnimationCancel(animation: Animator?) { @@ -119,11 +127,17 @@ class UnlockedScreenOffAnimationController @Inject constructor( // Run the callback given to us by the KeyguardVisibilityHelper. after.run() + + // Done going to sleep, reset this flag. + decidedToAnimateGoingToSleep = null } .start() } override fun onStartedWakingUp() { + // Waking up, so reset this flag. + decidedToAnimateGoingToSleep = null + lightRevealAnimator.cancel() handler.removeCallbacksAndMessages(null) } @@ -146,7 +160,9 @@ class UnlockedScreenOffAnimationController @Inject constructor( } override fun onStartedGoingToSleep() { - if (shouldPlayUnlockedScreenOffAnimation()) { + if (dozeParameters.get().shouldControlUnlockedScreenOff()) { + decidedToAnimateGoingToSleep = true + lightRevealAnimationPlaying = true lightRevealAnimator.start() @@ -156,6 +172,8 @@ class UnlockedScreenOffAnimationController @Inject constructor( // Show AOD. That'll cause the KeyguardVisibilityHelper to call #animateInKeyguard. statusBar.notificationPanelViewController.showAodUi() }, ANIMATE_IN_KEYGUARD_DELAY) + } else { + decidedToAnimateGoingToSleep = false } } @@ -164,6 +182,16 @@ class UnlockedScreenOffAnimationController @Inject constructor( * on the current state of the device. */ fun shouldPlayUnlockedScreenOffAnimation(): Boolean { + // If we explicitly already decided not to play the screen off animation, then never change + // our mind. + if (decidedToAnimateGoingToSleep == false) { + return false + } + + if (!dozeParameters.get().canControlUnlockedScreenOff()) { + return false + } + // We only play the unlocked screen off animation if we are... unlocked. if (statusBarStateControllerImpl.state != StatusBarState.SHADE) { return false @@ -173,7 +201,8 @@ class UnlockedScreenOffAnimationController @Inject constructor( // already expanded and showing notifications/QS, the animation looks really messy. For now, // disable it if the notification panel is expanded. if (!this::statusBar.isInitialized || - statusBar.notificationPanelViewController.isFullyExpanded) { + statusBar.notificationPanelViewController.isFullyExpanded || + statusBar.notificationPanelViewController.isExpanding) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt index 6e27caec9365..bb7ba4c4174f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt @@ -20,6 +20,7 @@ import android.content.Context import android.util.AttributeSet import android.widget.Chronometer +import androidx.annotation.UiThread /** * A [Chronometer] specifically for the ongoing call chip in the status bar. @@ -46,10 +47,10 @@ class OngoingCallChronometer @JvmOverloads constructor( // Minimum width that the text view can be. Corresponds with the largest number width seen so // far. - var minimumTextWidth: Int = 0 + private var minimumTextWidth: Int = 0 // True if the text is too long for the space available, so the text should be hidden. - var shouldHideText: Boolean = false + private var shouldHideText: Boolean = false override fun setBase(base: Long) { // These variables may have changed during the previous call, so re-set them before the new @@ -60,6 +61,13 @@ class OngoingCallChronometer @JvmOverloads constructor( super.setBase(base) } + /** Sets whether this view should hide its text or not. */ + @UiThread + fun setShouldHideText(shouldHideText: Boolean) { + this.shouldHideText = shouldHideText + requestLayout() + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { if (shouldHideText) { setMeasuredDimension(0, 0) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index b295f6659f81..16fa5da9e979 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -25,6 +25,7 @@ import android.content.Intent import android.util.Log import android.view.View import android.widget.Chronometer +import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator @@ -85,7 +86,7 @@ class OngoingCallController @Inject constructor( val newOngoingCallInfo = CallNotificationInfo( entry.sbn.key, entry.sbn.notification.`when`, - entry.sbn.notification.contentIntent.intent, + entry.sbn.notification.contentIntent?.intent, entry.sbn.uid, entry.sbn.notification.extras.getInt( Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING @@ -122,6 +123,7 @@ class OngoingCallController @Inject constructor( * Should only be called from [CollapsedStatusBarFragment]. */ fun setChipView(chipView: View) { + tearDownChipView() this.chipView = chipView if (hasOngoingCall()) { updateChip() @@ -165,25 +167,33 @@ class OngoingCallController @Inject constructor( val currentCallNotificationInfo = callNotificationInfo ?: return val currentChipView = chipView - val timeView = - currentChipView?.findViewById<Chronometer>(R.id.ongoing_call_chip_time) + val timeView = currentChipView?.getTimeView() val backgroundView = currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background) if (currentChipView != null && timeView != null && backgroundView != null) { - timeView.base = currentCallNotificationInfo.callStartTime - - System.currentTimeMillis() + - systemClock.elapsedRealtime() - timeView.start() - - currentChipView.setOnClickListener { - logger.logChipClicked() - activityStarter.postStartActivityDismissingKeyguard( - currentCallNotificationInfo.intent, 0, - ActivityLaunchAnimator.Controller.fromView( - backgroundView, - InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP) - ) + if (currentCallNotificationInfo.hasValidStartTime()) { + timeView.setShouldHideText(false) + timeView.base = currentCallNotificationInfo.callStartTime - + systemClock.currentTimeMillis() + + systemClock.elapsedRealtime() + timeView.start() + } else { + timeView.setShouldHideText(true) + timeView.stop() + } + + currentCallNotificationInfo.intent?.let { intent -> + currentChipView.setOnClickListener { + logger.logChipClicked() + activityStarter.postStartActivityDismissingKeyguard( + intent, + 0, + ActivityLaunchAnimator.Controller.fromView( + backgroundView, + InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP) + ) + } } setUpUidObserver(currentCallNotificationInfo) @@ -245,20 +255,35 @@ class OngoingCallController @Inject constructor( private fun removeChip() { callNotificationInfo = null + tearDownChipView() mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } if (uidObserver != null) { iActivityManager.unregisterUidObserver(uidObserver) } } + /** Tear down anything related to the chip view to prevent leaks. */ + @VisibleForTesting + fun tearDownChipView() = chipView?.getTimeView()?.stop() + + private fun View.getTimeView(): OngoingCallChronometer? { + return this.findViewById(R.id.ongoing_call_chip_time) + } + private data class CallNotificationInfo( val key: String, val callStartTime: Long, - val intent: Intent, + val intent: Intent?, val uid: Int, /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */ val isOngoing: Boolean - ) + ) { + /** + * Returns true if the notification information has a valid call start time. + * See b/192379214. + */ + fun hasValidStartTime(): Boolean = callStartTime > 0 + } } private fun isCallNotification(entry: NotificationEntry): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java index 399c8500ab48..a0edc7c494bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java @@ -74,11 +74,13 @@ public class BrightnessMirrorController mBrightnessMirror.setVisibility(View.VISIBLE); mVisibilityCallback.accept(true); mNotificationPanel.setPanelAlpha(0, true /* animate */); + mDepthController.setBrightnessMirrorVisible(true); } public void hideMirror() { mVisibilityCallback.accept(false); mNotificationPanel.setPanelAlpha(255, true /* animate */); + mDepthController.setBrightnessMirrorVisible(false); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 724a851107d5..081fe5a47626 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -33,6 +33,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dependency; +import com.android.systemui.EventLogTags; import com.android.systemui.R; import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -175,6 +176,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { NotificationEntry entry = alertEntry.mEntry; entry.setHeadsUp(true); setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry)); + EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpStateChanged(entry, true); } @@ -185,6 +187,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { NotificationEntry entry = alertEntry.mEntry; entry.setHeadsUp(false); setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */); + EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpStateChanged(entry, false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java index d7d1e737661e..acfdda4cea49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy; import android.hardware.SensorPrivacyManager.Sensors.Sensor; +import android.hardware.SensorPrivacyManager.Sources.Source; public interface IndividualSensorPrivacyController extends CallbackController<IndividualSensorPrivacyController.Callback> { @@ -26,7 +27,7 @@ public interface IndividualSensorPrivacyController extends boolean isSensorBlocked(@Sensor int sensor); - void setSensorBlocked(@Sensor int sensor, boolean blocked); + void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked); void suppressSensorPrivacyReminders(String packageName, boolean suppress); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java index f58a7c030b80..9807165f69d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java @@ -21,6 +21,7 @@ import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManager.Sensors.Sensor; +import android.hardware.SensorPrivacyManager.Sources.Source; import android.util.ArraySet; import android.util.SparseBooleanArray; @@ -62,8 +63,8 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr } @Override - public void setSensorBlocked(@Sensor int sensor, boolean blocked) { - mSensorPrivacyManager.setSensorPrivacyForProfileGroup(sensor, blocked); + public void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked) { + mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 0af1469b6c7c..23db06f8c899 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -51,7 +51,6 @@ import android.telephony.ims.ImsRegistrationAttributes; import android.telephony.ims.RegistrationManager.RegistrationCallback; import android.text.Html; import android.text.TextUtils; -import android.util.FeatureFlagUtils; import android.util.Log; import com.android.ims.ImsManager; @@ -75,6 +74,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.policy.FiveGServiceClient; import com.android.systemui.statusbar.policy.FiveGServiceClient.FiveGServiceState; import com.android.systemui.statusbar.policy.FiveGServiceClient.IFiveGStateListener; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; @@ -102,7 +102,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile private final String mNetworkNameDefault; private final String mNetworkNameSeparator; private final ContentObserver mObserver; - private final boolean mProviderModel; + private final boolean mProviderModelBehavior; + private final boolean mProviderModelSetting; private final Handler mReceiverHandler; private int mImsType = IMS_TYPE_WWAN; // Save entire info for logging, we only use the id. @@ -153,11 +154,19 @@ public class MobileSignalController extends SignalController<MobileState, Mobile // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't // need listener lists anymore. - public MobileSignalController(Context context, Config config, boolean hasMobileData, - TelephonyManager phone, CallbackHandler callbackHandler, - NetworkControllerImpl networkController, SubscriptionInfo info, - SubscriptionDefaults defaults, Looper receiverLooper, - CarrierConfigTracker carrierConfigTracker) { + public MobileSignalController( + Context context, + Config config, + boolean hasMobileData, + TelephonyManager phone, + CallbackHandler callbackHandler, + NetworkControllerImpl networkController, + SubscriptionInfo info, + SubscriptionDefaults defaults, + Looper receiverLooper, + CarrierConfigTracker carrierConfigTracker, + FeatureFlags featureFlags + ) { super("MobileSignalController(" + info.getSubscriptionId() + ")", context, NetworkCapabilities.TRANSPORT_CELLULAR, callbackHandler, networkController); @@ -286,8 +295,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile mImsMmTelManager = ImsMmTelManager.createForSubscriptionId(info.getSubscriptionId()); mMobileStatusTracker = new MobileStatusTracker(mPhone, receiverLooper, info, mDefaults, mCallback); - mProviderModel = FeatureFlagUtils.isEnabled( - mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); + mProviderModelBehavior = featureFlags.isCombinedStatusBarSignalIconsEnabled(); + mProviderModelSetting = featureFlags.isProviderModelSettingEnabled(); } public void setConfiguration(Config config) { @@ -340,7 +349,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile mContext.registerReceiver(mVolteSwitchObserver, new IntentFilter("org.codeaurora.intent.action.ACTION_ENHANCE_4G_SWITCH")); mFeatureConnector.connect(); - if (mProviderModel) { + if (mProviderModelBehavior) { mReceiverHandler.post(mTryRegisterIms); } } @@ -511,7 +520,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile || (mCurrentState.iconGroup == TelephonyIcons.NOT_DEFAULT_DATA)) && mCurrentState.userSetup; - if (mProviderModel) { + if (mProviderModelBehavior) { // Show icon in QS when we are connected or data is disabled. boolean showDataIcon = mCurrentState.dataConnected || dataDisabled; @@ -555,13 +564,26 @@ public class MobileSignalController extends SignalController<MobileState, Mobile IconState qsIcon = null; CharSequence description = null; // Only send data sim callbacks to QS. - if (mCurrentState.dataSim) { - qsTypeIcon = - (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0; - qsIcon = new IconState(mCurrentState.enabled - && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription); - description = mCurrentState.isEmergency ? null : mCurrentState.networkName; + if (mProviderModelSetting) { + if (mCurrentState.dataSim && mCurrentState.isDefault) { + qsTypeIcon = + (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0; + qsIcon = new IconState( + mCurrentState.enabled && !mCurrentState.isEmergency, + getQsCurrentIconId(), contentDescription); + description = mCurrentState.isEmergency ? null : mCurrentState.networkName; + } + } else { + if (mCurrentState.dataSim) { + qsTypeIcon = + (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0; + qsIcon = new IconState( + mCurrentState.enabled && !mCurrentState.isEmergency, + getQsCurrentIconId(), contentDescription); + description = mCurrentState.isEmergency ? null : mCurrentState.networkName; + } } + boolean activityIn = mCurrentState.dataConnected && !mCurrentState.carrierNetworkChangeMode && mCurrentState.activityIn; @@ -755,7 +777,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile // 1. The first valid voice state has been received // 2. The voice state has been changed and either the last or current state is // ServiceState.STATE_IN_SERVICE - if (mProviderModel + if (mProviderModelBehavior && lastVoiceState != currentVoiceState && (lastVoiceState == -1 || (lastVoiceState == ServiceState.STATE_IN_SERVICE @@ -829,7 +851,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile } void notifyWifiLevelChange(int level) { - if (!mProviderModel) { + if (!mProviderModelBehavior) { return; } mLastWlanLevel = level; @@ -844,7 +866,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile } void notifyDefaultMobileLevelChange(int level) { - if (!mProviderModel) { + if (!mProviderModelBehavior) { return; } mLastWlanCrossSimLevel = level; @@ -859,7 +881,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile } void notifyMobileLevelChangeIfNecessary(SignalStrength signalStrength) { - if (!mProviderModel) { + if (!mProviderModelBehavior) { return; } int newLevel = getSignalLevel(signalStrength); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 06974b356914..740b52d332ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -51,7 +51,6 @@ import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.text.TextUtils; -import android.util.FeatureFlagUtils; import android.util.Log; import android.util.MathUtils; import android.util.SparseArray; @@ -74,6 +73,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.CarrierConfigTracker; @@ -123,9 +123,11 @@ public class NetworkControllerImpl extends BroadcastReceiver private final BroadcastDispatcher mBroadcastDispatcher; private final DemoModeController mDemoModeController; private final Object mLock = new Object(); - private final boolean mProviderModel; + private final boolean mProviderModelBehavior; + private final boolean mProviderModelSetting; private Config mConfig; private final CarrierConfigTracker mCarrierConfigTracker; + private final FeatureFlags mFeatureFlags; private TelephonyCallback.ActiveDataSubscriptionIdListener mPhoneStateListener; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; @@ -220,7 +222,8 @@ public class NetworkControllerImpl extends BroadcastReceiver NetworkScoreManager networkScoreManager, AccessPointControllerImpl accessPointController, DemoModeController demoModeController, - CarrierConfigTracker carrierConfigTracker) { + CarrierConfigTracker carrierConfigTracker, + FeatureFlags featureFlags) { this(context, connectivityManager, telephonyManager, telephonyListenerManager, @@ -237,7 +240,8 @@ public class NetworkControllerImpl extends BroadcastReceiver deviceProvisionedController, broadcastDispatcher, demoModeController, - carrierConfigTracker); + carrierConfigTracker, + featureFlags); mReceiverHandler.post(mRegisterListeners); } @@ -256,7 +260,9 @@ public class NetworkControllerImpl extends BroadcastReceiver DeviceProvisionedController deviceProvisionedController, BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController, - CarrierConfigTracker carrierConfigTracker) { + CarrierConfigTracker carrierConfigTracker, + FeatureFlags featureFlags + ) { mContext = context; mTelephonyListenerManager = telephonyListenerManager; mConfig = config; @@ -273,6 +279,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mHasMobileDataFeature = telephonyManager.isDataCapable(); mDemoModeController = demoModeController; mCarrierConfigTracker = carrierConfigTracker; + mFeatureFlags = featureFlags; // telephony mPhone = telephonyManager; @@ -293,7 +300,8 @@ public class NetworkControllerImpl extends BroadcastReceiver } }); mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature, - mCallbackHandler, this, mWifiManager, mConnectivityManager, networkScoreManager); + mCallbackHandler, this, mWifiManager, mConnectivityManager, networkScoreManager, + mFeatureFlags); mEthernetSignalController = new EthernetSignalController(mContext, mCallbackHandler, this); @@ -422,8 +430,8 @@ public class NetworkControllerImpl extends BroadcastReceiver }; mDemoModeController.addCallback(this); - mProviderModel = FeatureFlagUtils.isEnabled( - mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); + mProviderModelBehavior = mFeatureFlags.isCombinedStatusBarSignalIconsEnabled(); + mProviderModelSetting = mFeatureFlags.isProviderModelSettingEnabled(); } private final Runnable mClearForceValidated = () -> { @@ -697,7 +705,7 @@ public class NetworkControllerImpl extends BroadcastReceiver cb.setIsAirplaneMode(new IconState(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode, mContext)); cb.setNoSims(mHasNoSubs, mSimDetected); - if (mProviderModel) { + if (mProviderModelBehavior) { cb.setConnectivityStatus(mNoDefaultNetwork, !mInetCondition, mNoNetworksAvailable); } mWifiSignalController.notifyListeners(cb); @@ -705,7 +713,7 @@ public class NetworkControllerImpl extends BroadcastReceiver for (int i = 0; i < mMobileSignalControllers.size(); i++) { MobileSignalController mobileSignalController = mMobileSignalControllers.valueAt(i); mobileSignalController.notifyListeners(cb); - if (mProviderModel) { + if (mProviderModelBehavior) { mobileSignalController.refreshCallIndicator(cb); } } @@ -806,7 +814,7 @@ public class NetworkControllerImpl extends BroadcastReceiver for (int i = 0; i < mMobileSignalControllers.size(); i++) { MobileSignalController controller = mMobileSignalControllers.valueAt(i); controller.setConfiguration(mConfig); - if (mProviderModel) { + if (mProviderModelBehavior) { controller.refreshCallIndicator(mCallbackHandler); } } @@ -922,7 +930,8 @@ public class NetworkControllerImpl extends BroadcastReceiver MobileSignalController controller = new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone.createForSubscriptionId(subId), mCallbackHandler, this, subscriptions.get(i), - mSubDefaults, mReceiverHandler.getLooper(), mCarrierConfigTracker); + mSubDefaults, mReceiverHandler.getLooper(), mCarrierConfigTracker, + mFeatureFlags); controller.setUserSetupComplete(mUserSetup); mMobileSignalControllers.put(subId, controller); if (subscriptions.get(i).getSimSlotIndex() == 0) { @@ -1070,10 +1079,10 @@ public class NetworkControllerImpl extends BroadcastReceiver || mValidatedTransports.get(NetworkCapabilities.TRANSPORT_ETHERNET); pushConnectivityToSignals(); - if (mProviderModel) { + if (mProviderModelBehavior) { mNoDefaultNetwork = !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_CELLULAR) - && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_WIFI) - && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_ETHERNET); + && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_WIFI) + && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_ETHERNET); mCallbackHandler.setConnectivityStatus(mNoDefaultNetwork, !mInetCondition, mNoNetworksAvailable); for (int i = 0; i < mMobileSignalControllers.size(); i++) { @@ -1081,6 +1090,13 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.updateNoCallingState(); } notifyAllListeners(); + } else if (mProviderModelSetting) { + // TODO(b/191903788): Replace the flag name once the new flag is added. + mNoDefaultNetwork = !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_CELLULAR) + && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_WIFI) + && !mConnectedTransports.get(NetworkCapabilities.TRANSPORT_ETHERNET); + mCallbackHandler.setConnectivityStatus(mNoDefaultNetwork, !mInetCondition, + mNoNetworksAvailable); } } @@ -1388,7 +1404,8 @@ public class NetworkControllerImpl extends BroadcastReceiver MobileSignalController controller = new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone.createForSubscriptionId(info.getSubscriptionId()), mCallbackHandler, this, - info, mSubDefaults, mReceiverHandler.getLooper(), mCarrierConfigTracker); + info, mSubDefaults, mReceiverHandler.getLooper(), mCarrierConfigTracker, + mFeatureFlags); mMobileSignalControllers.put(id, controller); controller.getState().userSetup = true; return info; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index e6c4e82cec7b..b18dfd2866c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -20,7 +20,6 @@ import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; @@ -38,7 +37,6 @@ import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.net.Uri; import android.os.Bundle; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.text.Editable; @@ -72,13 +70,13 @@ import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.graphics.ColorUtils; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dependency; import com.android.systemui.R; @@ -111,8 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private final SendButtonTextWatcher mTextWatcher; private final TextView.OnEditorActionListener mEditorActionHandler; - private final NotificationRemoteInputManager mRemoteInputManager; private final UiEventLogger mUiEventLogger; + private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; private final List<OnFocusChangeListener> mEditTextFocusChangeListeners = new ArrayList<>(); private final List<OnSendRemoteInputListener> mOnSendListeners = new ArrayList<>(); private RemoteEditText mEditText; @@ -123,9 +121,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private RemoteInput[] mRemoteInputs; private RemoteInput mRemoteInput; private RemoteInputController mController; - private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; - - private IStatusBarService mStatusBarManagerService; private NotificationEntry mEntry; @@ -134,7 +129,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private int mRevealCx; private int mRevealCy; private int mRevealR; - private ContentInfo mAttachment; private boolean mColorized; private int mTint; @@ -143,7 +137,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private NotificationViewWrapper mWrapper; private Consumer<Boolean> mOnVisibilityChangedListener; private NotificationRemoteInputManager.BouncerChecker mBouncerChecker; - private LinearLayout mContentView; private ImageView mDelete; private ImageView mDeleteBg; @@ -174,10 +167,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mTextWatcher = new SendButtonTextWatcher(); mEditorActionHandler = new EditorActionHandler(); mRemoteInputQuickSettingsDisabler = Dependency.get(RemoteInputQuickSettingsDisabler.class); - mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class); mUiEventLogger = Dependency.get(UiEventLogger.class); - mStatusBarManagerService = IStatusBarService.Stub.asInterface( - ServiceManager.getService(Context.STATUS_BAR_SERVICE)); TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ com.android.internal.R.attr.colorAccent, com.android.internal.R.attr.colorSurface, @@ -266,8 +256,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN); mDelete.setImageTintBlendMode(BlendMode.SRC_IN); mDelete.setOnClickListener(v -> setAttachment(null)); - mContentView = findViewById(R.id.remote_input_content); - mContentView.setBackground(mContentBackground); + LinearLayout contentView = findViewById(R.id.remote_input_content); + contentView.setBackground(mContentBackground); mEditText = findViewById(R.id.remote_input_text); mEditText.setInnerFocusable(false); mEditText.setWindowInsetsAnimationCallback( @@ -293,15 +283,19 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void setAttachment(ContentInfo item) { - if (mAttachment != null) { + if (mEntry.remoteInputAttachment != null && mEntry.remoteInputAttachment != item) { // We need to release permissions when sending the attachment to the target // app or if it is deleted by the user. When sending to the target app, we // can safely release permissions as soon as the call to // `mController.grantInlineReplyUriPermission` is made (ie, after the grant // to the target app has been created). - mAttachment.releasePermissions(); + mEntry.remoteInputAttachment.releasePermissions(); + } + mEntry.remoteInputAttachment = item; + if (item != null) { + mEntry.remoteInputUri = item.getClip().getItemAt(0).getUri(); + mEntry.remoteInputMimeType = item.getClip().getDescription().getMimeType(0); } - mAttachment = item; View attachment = findViewById(R.id.remote_input_content_container); ImageView iconView = findViewById(R.id.remote_input_attachment_image); iconView.setImageDrawable(null); @@ -323,10 +317,9 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene * @return returns intent with granted URI permissions that should be used immediately */ private Intent prepareRemoteInput() { - if (mAttachment == null) return prepareRemoteInputFromText(); - return prepareRemoteInputFromData( - mAttachment.getClip().getDescription().getMimeType(0), - mAttachment.getClip().getItemAt(0).getUri()); + return mEntry.remoteInputAttachment == null + ? prepareRemoteInputFromText() + : prepareRemoteInputFromData(mEntry.remoteInputMimeType, mEntry.remoteInputUri); } private Intent prepareRemoteInputFromText() { @@ -337,7 +330,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene results); mEntry.remoteInputText = mEditText.getText().toString(); - // TODO(b/188646667): store attachment to entry + setAttachment(null); mEntry.remoteInputUri = null; mEntry.remoteInputMimeType = null; @@ -363,7 +356,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, bundle); - CharSequence attachmentText = mAttachment.getClip().getDescription().getLabel(); + CharSequence attachmentText = + mEntry.remoteInputAttachment.getClip().getDescription().getLabel(); CharSequence attachmentLabel = TextUtils.isEmpty(attachmentText) ? mContext.getString(R.string.remote_input_image_insertion_text) @@ -374,14 +368,11 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene : "\"" + attachmentLabel + "\" " + mEditText.getText(); mEntry.remoteInputText = fullText; - // TODO(b/188646667): store attachment to entry - mEntry.remoteInputMimeType = contentType; - mEntry.remoteInputUri = data; // mirror prepareRemoteInputFromText for text input if (mEntry.editedSuggestionInfo == null) { RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT); - } else if (mAttachment == null) { + } else if (mEntry.remoteInputAttachment == null) { RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE); } @@ -437,6 +428,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), mEntry.getSbn().getInstanceId()); } + setAttachment(null); } @@ -477,7 +469,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private void onDefocus(boolean animate, boolean logClose) { mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); - // TODO(b/188646667): store attachment to entry // During removal, we get reattached and lose focus. Not hiding in that // case to prevent flicker. @@ -565,7 +556,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEntry.editedSuggestionInfo = editedSuggestionInfo; if (editedSuggestionInfo != null) { mEntry.remoteInputText = editedSuggestionInfo.originalText; - // TODO(b/188646667): store attachment to entry + mEntry.remoteInputAttachment = null; } } @@ -608,7 +599,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEditText.setSelection(mEditText.length()); mEditText.requestFocus(); mController.addRemoteInput(mEntry, mToken); - // TODO(b/188646667): restore attachment from entry + setAttachment(mEntry.remoteInputAttachment); mRemoteInputQuickSettingsDisabler.setRemoteInputActive(true); @@ -631,7 +622,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private void reset() { mResetting = true; mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); - // TODO(b/188646667): store attachment at time of reset to entry mEditText.getText().clear(); mEditText.setEnabled(true); @@ -640,7 +630,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); onDefocus(false /* animate */, false /* logClose */); - // TODO(b/188646667): clear attachment + setAttachment(null); mResetting = false; } @@ -657,7 +647,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void updateSendButton() { - mSendButton.setEnabled(mEditText.length() != 0 || mAttachment != null); + mSendButton.setEnabled(mEditText.length() != 0 || mEntry.remoteInputAttachment != null); } public void close() { @@ -857,7 +847,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene && event.getAction() == KeyEvent.ACTION_DOWN; if (isSoftImeEvent || isKeyboardEnterKey) { - if (mEditText.length() > 0 || mAttachment != null) { + if (mEditText.length() > 0 || mEntry.remoteInputAttachment != null) { sendRemoteInput(prepareRemoteInput()); } // Consume action to prevent IME from closing. @@ -928,7 +918,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // our focus, so we'll need to save our text here. if (mRemoteInputView != null) { mRemoteInputView.mEntry.remoteInputText = getText(); - // TODO(b/188646667): store attachment to entry } } return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java index e76b8035cd59..2a93844acd5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java @@ -28,6 +28,8 @@ public interface SecurityController extends CallbackController<SecurityControlle boolean isDeviceManaged(); boolean hasProfileOwner(); boolean hasWorkProfile(); + /** Whether the work profile is turned on. */ + boolean isWorkProfileOn(); /** Whether this device is organization-owned with a work profile **/ boolean isProfileOwnerOfOrganizationOwnedDevice(); String getDeviceOwnerName(); @@ -57,7 +59,6 @@ public interface SecurityController extends CallbackController<SecurityControlle /** Label for admin */ CharSequence getLabel(DeviceAdminInfo info); - public interface SecurityControllerCallback { void onStateChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 4afb86b1a810..3e661df802a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -211,6 +211,12 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi } @Override + public boolean isWorkProfileOn() { + final UserHandle userHandle = UserHandle.of(getWorkProfileUserId(mCurrentUserId)); + return userHandle != null && !mUserManager.isQuietModeEnabled(userHandle); + } + + @Override public boolean isProfileOwnerOfOrganizationOwnedDevice() { return mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 1ebb9ddc0262..e2b6895e7039 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -671,6 +671,7 @@ public class UserSwitcherController implements Dumpable { scheduleGuestCreation(); } switchToUserId(targetUserId); + mUserManager.removeUser(currentUser.id); } } catch (RemoteException e) { Log.e(TAG, "Couldn't remove guest because ActivityManager or WindowManager is dead"); @@ -1012,15 +1013,16 @@ public class UserSwitcherController implements Dumpable { public ExitGuestDialog(Context context, int guestId, int targetId) { super(context); - setTitle(mGuestUserAutoCreated ? R.string.guest_reset_guest_dialog_title + setTitle(mGuestUserAutoCreated + ? com.android.settingslib.R.string.guest_reset_guest_dialog_title : R.string.guest_exit_guest_dialog_title); setMessage(context.getString(R.string.guest_exit_guest_dialog_message)); setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this); setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - mGuestUserAutoCreated ? R.string.guest_reset_guest_dialog_remove - : R.string.guest_exit_guest_dialog_remove), this); + context.getString(mGuestUserAutoCreated + ? com.android.settingslib.R.string.guest_reset_guest_confirm_button + : R.string.guest_exit_guest_dialog_remove), this); SystemUIDialog.setWindowOnTop(this); setCanceledOnTouchOutside(false); mGuestId = guestId; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java index ea103e6f02a8..cb96bd80a90c 100755 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -27,7 +27,6 @@ import android.net.NetworkScoreManager; import android.net.wifi.WifiManager; import android.text.Html; import android.text.TextUtils; -import android.util.FeatureFlagUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.AccessibilityContentDescriptions; @@ -38,6 +37,7 @@ import com.android.settingslib.graph.SignalDrawable; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.wifi.WifiStatusTracker; import com.android.systemui.R; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; @@ -53,17 +53,22 @@ public class WifiSignalController extends private final IconGroup mUnmergedWifiIconGroup = WifiIcons.UNMERGED_WIFI; private final MobileIconGroup mCarrierMergedWifiIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI; private final WifiManager mWifiManager; - private final boolean mProviderModel; + private final boolean mProviderModelSetting; private final IconGroup mDefaultWifiIconGroup; private final IconGroup mWifi4IconGroup; private final IconGroup mWifi5IconGroup; private final IconGroup mWifi6IconGroup; - public WifiSignalController(Context context, boolean hasMobileDataFeature, - CallbackHandler callbackHandler, NetworkControllerImpl networkController, - WifiManager wifiManager, ConnectivityManager connectivityManager, - NetworkScoreManager networkScoreManager) { + public WifiSignalController( + Context context, + boolean hasMobileDataFeature, + CallbackHandler callbackHandler, + NetworkControllerImpl networkController, + WifiManager wifiManager, + ConnectivityManager connectivityManager, + NetworkScoreManager networkScoreManager, + FeatureFlags featureFlags) { super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, callbackHandler, networkController); mWifiManager = wifiManager; @@ -125,8 +130,8 @@ public class WifiSignalController extends ); mCurrentState.iconGroup = mLastState.iconGroup = mDefaultWifiIconGroup; - mProviderModel = FeatureFlagUtils.isEnabled( - mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); + mProviderModelSetting = featureFlags.isProviderModelSettingEnabled(); + } @Override @@ -163,7 +168,7 @@ public class WifiSignalController extends if (mCurrentState.inetCondition == 0) { contentDescription += ("," + mContext.getString(R.string.data_connection_no_internet)); } - if (mProviderModel) { + if (mProviderModelSetting) { IconState statusIcon = new IconState( wifiVisible, getCurrentIconId(), contentDescription); IconState qsIcon = null; diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index e0ff88bdbfce..843630b35e17 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -239,6 +239,14 @@ public class ThemeOverlayApplier implements Dumpable { + category + ": " + enabled); } + OverlayInfo overlayInfo = mOverlayManager.getOverlayInfo(identifier, + UserHandle.of(currentUser)); + if (overlayInfo == null) { + Log.i(TAG, "Won't enable " + identifier + ", it doesn't exist for user" + + currentUser); + return; + } + transaction.setEnabled(identifier, enabled, currentUser); if (currentUser != UserHandle.SYSTEM.getIdentifier() && SYSTEM_USER_CATEGORIES.contains(category)) { @@ -247,7 +255,7 @@ public class ThemeOverlayApplier implements Dumpable { // Do not apply Launcher or Theme picker overlays to managed users. Apps are not // installed in there. - OverlayInfo overlayInfo = mOverlayManager.getOverlayInfo(identifier, UserHandle.SYSTEM); + overlayInfo = mOverlayManager.getOverlayInfo(identifier, UserHandle.SYSTEM); if (overlayInfo == null || overlayInfo.targetPackageName.equals(mLauncherPackage) || overlayInfo.targetPackageName.equals(mThemePickerPackage)) { return; diff --git a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java index c5e35a497956..8b394bfe35b7 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java +++ b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java @@ -16,13 +16,18 @@ package com.android.systemui.toast; +import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; +import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; + import android.animation.Animator; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -53,7 +58,7 @@ public class SystemUIToast implements ToastPlugin.Toast { final ToastPlugin.Toast mPluginToast; private final String mPackageName; - private final int mUserId; + @UserIdInt private final int mUserId; private final LayoutInflater mLayoutInflater; final int mDefaultX = 0; @@ -74,7 +79,7 @@ public class SystemUIToast implements ToastPlugin.Toast { } SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, - ToastPlugin.Toast pluginToast, String packageName, int userId, + ToastPlugin.Toast pluginToast, String packageName, @UserIdInt int userId, int orientation) { mLayoutInflater = layoutInflater; mContext = context; @@ -248,6 +253,15 @@ public class SystemUIToast implements ToastPlugin.Toast { return null; } + final Context userContext; + try { + userContext = context.createPackageContextAsUser("android", + 0, new UserHandle(userId)); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not create user package context"); + return null; + } + final ApplicationsState appState = ApplicationsState.getInstance((Application) context.getApplicationContext()); if (!appState.isUserAdded(userId)) { @@ -255,9 +269,11 @@ public class SystemUIToast implements ToastPlugin.Toast { + "packageName=" + packageName); return null; } + + final PackageManager packageManager = userContext.getPackageManager(); final AppEntry appEntry = appState.getEntry(packageName, userId); if (appEntry == null || appEntry.info == null - || !ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(appEntry)) { + || !showApplicationIcon(appEntry.info, packageManager)) { return null; } @@ -265,7 +281,20 @@ public class SystemUIToast implements ToastPlugin.Toast { UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid); IconFactory iconFactory = IconFactory.obtain(context); Bitmap iconBmp = iconFactory.createBadgedIconBitmap( - appInfo.loadUnbadgedIcon(context.getPackageManager()), user, true).icon; + appInfo.loadUnbadgedIcon(packageManager), user, true).icon; return new BitmapDrawable(context.getResources(), iconBmp); } + + private static boolean showApplicationIcon(ApplicationInfo appInfo, + PackageManager packageManager) { + if (hasFlag(appInfo.flags, FLAG_UPDATED_SYSTEM_APP)) { + return packageManager.getLaunchIntentForPackage(appInfo.packageName) + != null; + } + return !hasFlag(appInfo.flags, FLAG_SYSTEM); + } + + private static boolean hasFlag(int flags, int flag) { + return (flags & flag) != 0; + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt index dc86d5893adb..3b64f9f953c3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt @@ -16,6 +16,7 @@ package com.android.systemui.util +import android.content.pm.ActivityInfo import android.content.res.Resources import android.graphics.Rect import android.graphics.drawable.Drawable @@ -64,6 +65,10 @@ class RoundedCornerProgressDrawable @JvmOverloads constructor( return RoundedCornerState(super.getConstantState()!!) } + override fun getChangingConfigurations(): Int { + return super.getChangingConfigurations() or ActivityInfo.CONFIG_DENSITY + } + private class RoundedCornerState(private val wrappedState: ConstantState) : ConstantState() { override fun newDrawable(): Drawable { return newDrawable(null, null) diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java index 7f3756244629..11e7df8bd85f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java @@ -36,12 +36,13 @@ public class SensorModule { try { return thresholdSensorBuilder .setSensorDelay(SensorManager.SENSOR_DELAY_NORMAL) - .setSensorResourceId(R.string.proximity_sensor_type) + .setSensorResourceId(R.string.proximity_sensor_type, true) .setThresholdResourceId(R.dimen.proximity_sensor_threshold) .setThresholdLatchResourceId(R.dimen.proximity_sensor_threshold_latch) .build(); } catch (IllegalStateException e) { - Sensor defaultSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + Sensor defaultSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY, + true); return thresholdSensorBuilder .setSensor(defaultSensor) .setThresholdValue(defaultSensor != null ? defaultSensor.getMaximumRange() : 0) @@ -55,7 +56,7 @@ public class SensorModule { ThresholdSensorImpl.Builder thresholdSensorBuilder) { try { return thresholdSensorBuilder - .setSensorResourceId(R.string.proximity_sensor_secondary_type) + .setSensorResourceId(R.string.proximity_sensor_secondary_type, true) .setThresholdResourceId(R.dimen.proximity_sensor_secondary_threshold) .setThresholdLatchResourceId(R.dimen.proximity_sensor_secondary_threshold_latch) .build(); diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java index 31c307297066..d10cf9b180c3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java @@ -230,14 +230,16 @@ class ThresholdSensorImpl implements ThresholdSensor { mExecution = execution; } - Builder setSensorDelay(int sensorDelay) { mSensorDelay = sensorDelay; return this; } - - Builder setSensorResourceId(int sensorResourceId) { - setSensorType(mResources.getString(sensorResourceId)); + /** + * If requiresWakeUp is false, the first sensor with sensorType (regardless of whether the + * sensor is a wakeup sensor or not) will be set. + */ + Builder setSensorResourceId(int sensorResourceId, boolean requireWakeUp) { + setSensorType(mResources.getString(sensorResourceId), requireWakeUp); return this; } @@ -259,8 +261,12 @@ class ThresholdSensorImpl implements ThresholdSensor { return this; } - Builder setSensorType(String sensorType) { - Sensor sensor = findSensorByType(sensorType); + /** + * If requiresWakeUp is false, the first sensor with sensorType (regardless of whether the + * sensor is a wakeup sensor or not) will be set. + */ + Builder setSensorType(String sensorType, boolean requireWakeUp) { + Sensor sensor = findSensorByType(sensorType, requireWakeUp); if (sensor != null) { setSensor(sensor); } @@ -310,7 +316,8 @@ class ThresholdSensorImpl implements ThresholdSensor { mThresholdValue, mThresholdLatchValue, mSensorDelay); } - private Sensor findSensorByType(String sensorType) { + @VisibleForTesting + Sensor findSensorByType(String sensorType, boolean requireWakeUp) { if (sensorType.isEmpty()) { return null; } @@ -320,7 +327,9 @@ class ThresholdSensorImpl implements ThresholdSensor { for (Sensor s : sensorList) { if (sensorType.equals(s.getStringType())) { sensor = s; - break; + if (!requireWakeUp || sensor.isWakeUpSensor()) { + break; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index ab4b1f10132c..e57059894786 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -128,7 +128,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final MediaSessions mMediaSessions; protected C mCallbacks = new C(); private final State mState = new State(); - protected final MediaSessionsCallbacks mMediaSessionsCallbacksW = new MediaSessionsCallbacks(); + protected final MediaSessionsCallbacks mMediaSessionsCallbacksW; private final Optional<Vibrator> mVibrator; private final boolean mHasVibrator; private boolean mShowA11yStream; @@ -179,6 +179,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mWorkerLooper = theadFactory.buildLooperOnNewThread( VolumeDialogControllerImpl.class.getSimpleName()); mWorker = new W(mWorkerLooper); + mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext); mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW); mAudio = audioManager; mNoMan = notificationManager; @@ -1148,83 +1149,98 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); private int mNextStream = DYNAMIC_STREAM_START_INDEX; + private final boolean mShowRemoteSessions; + + public MediaSessionsCallbacks(Context context) { + mShowRemoteSessions = context.getResources().getBoolean( + com.android.internal.R.bool.config_volumeShowRemoteSessions); + } @Override public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { - addStream(token, "onRemoteUpdate"); + if (mShowRemoteSessions) { + addStream(token, "onRemoteUpdate"); - int stream = 0; - synchronized (mRemoteStreams) { - stream = mRemoteStreams.get(token); - } - Slog.d(TAG, "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume()); - boolean changed = mState.states.indexOfKey(stream) < 0; - final StreamState ss = streamStateW(stream); - ss.dynamic = true; - ss.levelMin = 0; - ss.levelMax = pi.getMaxVolume(); - if (ss.level != pi.getCurrentVolume()) { - ss.level = pi.getCurrentVolume(); - changed = true; - } - if (!Objects.equals(ss.remoteLabel, name)) { - ss.name = -1; - ss.remoteLabel = name; - changed = true; - } - if (changed) { - Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax); - mCallbacks.onStateChanged(mState); + int stream = 0; + synchronized (mRemoteStreams) { + stream = mRemoteStreams.get(token); + } + Slog.d(TAG, + "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume()); + boolean changed = mState.states.indexOfKey(stream) < 0; + final StreamState ss = streamStateW(stream); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = pi.getMaxVolume(); + if (ss.level != pi.getCurrentVolume()) { + ss.level = pi.getCurrentVolume(); + changed = true; + } + if (!Objects.equals(ss.remoteLabel, name)) { + ss.name = -1; + ss.remoteLabel = name; + changed = true; + } + if (changed) { + Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax); + mCallbacks.onStateChanged(mState); + } } } @Override public void onRemoteVolumeChanged(Token token, int flags) { - addStream(token, "onRemoteVolumeChanged"); - int stream = 0; - synchronized (mRemoteStreams) { - stream = mRemoteStreams.get(token); - } - final boolean showUI = shouldShowUI(flags); - Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI); - boolean changed = updateActiveStreamW(stream); - if (showUI) { - changed |= checkRoutedToBluetoothW(AudioManager.STREAM_MUSIC); - } - if (changed) { - Slog.d(TAG, "onRemoteChanged: updatingState"); - mCallbacks.onStateChanged(mState); - } - if (showUI) { - mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED); + if (mShowRemoteSessions) { + addStream(token, "onRemoteVolumeChanged"); + int stream = 0; + synchronized (mRemoteStreams) { + stream = mRemoteStreams.get(token); + } + final boolean showUI = shouldShowUI(flags); + Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI); + boolean changed = updateActiveStreamW(stream); + if (showUI) { + changed |= checkRoutedToBluetoothW(AudioManager.STREAM_MUSIC); + } + if (changed) { + Slog.d(TAG, "onRemoteChanged: updatingState"); + mCallbacks.onStateChanged(mState); + } + if (showUI) { + mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED); + } } } @Override public void onRemoteRemoved(Token token) { - int stream = 0; - synchronized (mRemoteStreams) { - if (!mRemoteStreams.containsKey(token)) { - Log.d(TAG, "onRemoteRemoved: stream doesn't exist, " - + "aborting remote removed for token:" + token.toString()); - return; + if (mShowRemoteSessions) { + int stream = 0; + synchronized (mRemoteStreams) { + if (!mRemoteStreams.containsKey(token)) { + Log.d(TAG, "onRemoteRemoved: stream doesn't exist, " + + "aborting remote removed for token:" + token.toString()); + return; + } + stream = mRemoteStreams.get(token); } - stream = mRemoteStreams.get(token); - } - mState.states.remove(stream); - if (mState.activeStream == stream) { - updateActiveStreamW(-1); + mState.states.remove(stream); + if (mState.activeStream == stream) { + updateActiveStreamW(-1); + } + mCallbacks.onStateChanged(mState); } - mCallbacks.onStateChanged(mState); } public void setStreamVolume(int stream, int level) { - final Token t = findToken(stream); - if (t == null) { - Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); - return; + if (mShowRemoteSessions) { + final Token t = findToken(stream); + if (t == null) { + Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); + return; + } + mMediaSessions.setVolume(t, level); } - mMediaSessions.setVolume(t, level); } private Token findToken(int stream) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 407b248cee44..5de7846a820e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1127,7 +1127,12 @@ public class VolumeDialogImpl implements VolumeDialog, .alpha(0.f) .setStartDelay(0) .setDuration(mDialogHideAnimationDurationMs) - .withEndAction(() -> mODICaptionsTooltipView.setVisibility(INVISIBLE)) + .withEndAction(() -> { + // It might have been nulled out by tryToRemoveCaptionsTooltip. + if (mODICaptionsTooltipView != null) { + mODICaptionsTooltipView.setVisibility(INVISIBLE); + } + }) .start(); } } diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java index 65f236b77a64..0ecc4e25047f 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java @@ -25,6 +25,7 @@ import android.provider.Settings; import android.service.quickaccesswallet.GetWalletCardsRequest; import android.service.quickaccesswallet.QuickAccessWalletClient; import android.service.quickaccesswallet.QuickAccessWalletClientImpl; +import android.util.Log; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; @@ -142,6 +143,10 @@ public class QuickAccessWalletController { */ public void queryWalletCards( QuickAccessWalletClient.OnWalletCardsRetrievedCallback cardsRetriever) { + if (!mQuickAccessWalletClient.isWalletFeatureAvailable()) { + Log.d(TAG, "QuickAccessWallet feature is not available."); + return; + } int cardWidth = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_width); int cardHeight = diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java index 2dcc43cf60dc..2b4b49b82df1 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java @@ -34,10 +34,12 @@ import android.widget.Toolbar; import androidx.annotation.NonNull; +import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.Utils; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; @@ -65,9 +67,11 @@ public class WalletActivity extends LifecycleActivity implements private final Executor mExecutor; private final Handler mHandler; private final FalsingManager mFalsingManager; + private FalsingCollector mFalsingCollector; private final UserTracker mUserTracker; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final StatusBarKeyguardViewManager mKeyguardViewManager; + private final UiEventLogger mUiEventLogger; private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; private WalletScreenController mWalletScreenController; @@ -82,18 +86,22 @@ public class WalletActivity extends LifecycleActivity implements @Background Executor executor, @Main Handler handler, FalsingManager falsingManager, + FalsingCollector falsingCollector, UserTracker userTracker, KeyguardUpdateMonitor keyguardUpdateMonitor, - StatusBarKeyguardViewManager keyguardViewManager) { + StatusBarKeyguardViewManager keyguardViewManager, + UiEventLogger uiEventLogger) { mKeyguardStateController = keyguardStateController; mKeyguardDismissUtil = keyguardDismissUtil; mActivityStarter = activityStarter; mExecutor = executor; mHandler = handler; mFalsingManager = falsingManager; + mFalsingCollector = falsingCollector; mUserTracker = userTracker; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardViewManager = keyguardViewManager; + mUiEventLogger = uiEventLogger; } @Override @@ -125,7 +133,8 @@ public class WalletActivity extends LifecycleActivity implements mUserTracker, mFalsingManager, mKeyguardUpdateMonitor, - mKeyguardStateController); + mKeyguardStateController, + mUiEventLogger); mKeyguardUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @Override public void onBiometricRunningStateChanged( @@ -136,7 +145,8 @@ public class WalletActivity extends LifecycleActivity implements } }; - walletView.getAppButton().setOnClickListener( + walletView.setFalsingCollector(mFalsingCollector); + walletView.setShowWalletAppOnClickListener( v -> { if (mWalletClient.createWalletIntent() == null) { Log.w(TAG, "Unable to create wallet app intent."); @@ -148,11 +158,14 @@ public class WalletActivity extends LifecycleActivity implements } if (mKeyguardStateController.isUnlocked()) { + mUiEventLogger.log(WalletUiEvent.QAW_SHOW_ALL); mActivityStarter.startActivity( mWalletClient.createWalletIntent(), true); finish(); } else { + mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_SHOW_ALL_BUTTON); mKeyguardDismissUtil.executeWhenUnlocked(() -> { + mUiEventLogger.log(WalletUiEvent.QAW_SHOW_ALL); mActivityStarter.startActivity( mWalletClient.createWalletIntent(), true); finish(); @@ -170,6 +183,7 @@ public class WalletActivity extends LifecycleActivity implements return; } + mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_UNLOCK_BUTTON); mKeyguardDismissUtil.executeWhenUnlocked(() -> false, false, false); }); diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java index ab8ad7779689..2e183b38a7dc 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java @@ -39,6 +39,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; @@ -74,6 +75,7 @@ public class WalletScreenController implements private final WalletView mWalletView; private final WalletCardCarousel mCardCarousel; private final FalsingManager mFalsingManager; + private final UiEventLogger mUiEventLogger; @VisibleForTesting String mSelectedCardId; @VisibleForTesting boolean mIsDismissed; @@ -88,7 +90,8 @@ public class WalletScreenController implements UserTracker userTracker, FalsingManager falsingManager, KeyguardUpdateMonitor keyguardUpdateMonitor, - KeyguardStateController keyguardStateController) { + KeyguardStateController keyguardStateController, + UiEventLogger uiEventLogger) { mContext = context; mWalletClient = walletClient; mActivityStarter = activityStarter; @@ -97,6 +100,7 @@ public class WalletScreenController implements mFalsingManager = falsingManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardStateController = keyguardStateController; + mUiEventLogger = uiEventLogger; mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); mWalletView = walletView; mWalletView.setMinimumHeight(getExpectedMinHeight()); @@ -147,6 +151,7 @@ public class WalletScreenController implements isUdfpsEnabled); } } + mUiEventLogger.log(WalletUiEvent.QAW_IMPRESSION); removeMinHeightAndRecordHeightOnLayout(); }); } @@ -180,6 +185,9 @@ public class WalletScreenController implements if (mIsDismissed) { return; } + if (mSelectedCardId != null && !mSelectedCardId.equals(card.getCardId())) { + mUiEventLogger.log(WalletUiEvent.QAW_CHANGE_CARD); + } mSelectedCardId = card.getCardId(); selectCard(); } @@ -209,6 +217,12 @@ public class WalletScreenController implements || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) { return; } + + if (!mKeyguardStateController.isUnlocked()) { + mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_CARD_CLICK); + } + mUiEventLogger.log(WalletUiEvent.QAW_CLICK_CARD); + mActivityStarter.startActivity( ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent().getIntent(), true); } diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletUiEvent.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletUiEvent.java new file mode 100644 index 000000000000..da3a5c619446 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletUiEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wallet.ui; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +/** + * Ui events for the Quick Access Wallet. + */ +public enum WalletUiEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "The default payment app is opened to show all payment cards.") + QAW_SHOW_ALL(860), + + @UiEvent(doc = "The Quick Access Wallet homescreen is unlocked.") + QAW_UNLOCK_FROM_CARD_CLICK(861), + + @UiEvent(doc = "The Quick Access Wallet center card is changed") + QAW_CHANGE_CARD(863), + + @UiEvent(doc = "The Quick Access Wallet is opened.") + QAW_IMPRESSION(864), + + @UiEvent(doc = "The Quick Access Wallet card is clicked") + QAW_CLICK_CARD(865), + + @UiEvent(doc = "The Quick Access Wallet homescreen is unlocked via clicking the unlock button") + QAW_UNLOCK_FROM_UNLOCK_BUTTON(866), + + @UiEvent( + doc = "The Quick Access Wallet homescreen is unlocked via clicking the show all button") + QAW_UNLOCK_FROM_SHOW_ALL_BUTTON(867); + + private final int mId; + + WalletUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java index 0c5347724035..420f84abe0dd 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java @@ -22,6 +22,7 @@ import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_ import android.annotation.Nullable; import android.app.PendingIntent; import android.content.Context; +import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; @@ -39,6 +40,7 @@ import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.Utils; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; import java.util.List; @@ -54,6 +56,8 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard private final TextView mCardLabel; // Displays at the bottom of the screen, allow user to enter the default wallet app. private final Button mAppButton; + // Displays on the top right of the screen, allow user to enter the default wallet app. + private final Button mToolbarAppButton; // Displays underneath the carousel, allow user to unlock device, verify card, etc. private final Button mActionButton; private final Interpolator mOutInterpolator; @@ -61,10 +65,11 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard private final ViewGroup mCardCarouselContainer; private final TextView mErrorView; private final ViewGroup mEmptyStateView; - private CharSequence mCenterCardText; private boolean mIsDeviceLocked = false; private boolean mIsUdfpsEnabled = false; private OnClickListener mDeviceLockedActionOnClickListener; + private OnClickListener mShowWalletAppOnClickListener; + private FalsingCollector mFalsingCollector; public WalletView(Context context) { this(context, null); @@ -79,6 +84,7 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard mIcon = requireViewById(R.id.icon); mCardLabel = requireViewById(R.id.label); mAppButton = requireViewById(R.id.wallet_app_button); + mToolbarAppButton = requireViewById(R.id.wallet_toolbar_app_button); mActionButton = requireViewById(R.id.wallet_action_button); mErrorView = requireViewById(R.id.error_view); mEmptyStateView = requireViewById(R.id.wallet_empty_state); @@ -94,6 +100,43 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard } @Override + protected void onConfigurationChanged(Configuration newConfig) { + updateViewForOrientation(newConfig.orientation); + } + + private void updateViewForOrientation(@Configuration.Orientation int orientation) { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + renderViewPortrait(); + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + renderViewLandscape(); + } + ViewGroup.LayoutParams params = mCardCarouselContainer.getLayoutParams(); + if (params instanceof MarginLayoutParams) { + ((MarginLayoutParams) params).topMargin = + getResources().getDimensionPixelSize( + R.dimen.wallet_card_carousel_container_top_margin); + } + } + + private void renderViewPortrait() { + mAppButton.setVisibility(VISIBLE); + mToolbarAppButton.setVisibility(GONE); + mCardLabel.setVisibility(VISIBLE); + requireViewById(R.id.dynamic_placeholder).setVisibility(VISIBLE); + + mAppButton.setOnClickListener(mShowWalletAppOnClickListener); + } + + private void renderViewLandscape() { + mToolbarAppButton.setVisibility(VISIBLE); + mAppButton.setVisibility(GONE); + mCardLabel.setVisibility(GONE); + requireViewById(R.id.dynamic_placeholder).setVisibility(GONE); + + mToolbarAppButton.setOnClickListener(mShowWalletAppOnClickListener); + } + + @Override public boolean onTouchEvent(MotionEvent event) { // Forward touch events to card carousel to allow for swiping outside carousel bounds. return mCardCarousel.onTouchEvent(event) || super.onTouchEvent(event); @@ -137,10 +180,12 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard mIsDeviceLocked = isDeviceLocked; mIsUdfpsEnabled = isUdfpsEnabled; mCardCarouselContainer.setVisibility(VISIBLE); + mCardCarousel.setVisibility(VISIBLE); mErrorView.setVisibility(GONE); mEmptyStateView.setVisibility(GONE); mIcon.setImageDrawable(getHeaderIcon(mContext, data.get(selectedIndex))); mCardLabel.setText(getLabelText(data.get(selectedIndex))); + updateViewForOrientation(getResources().getConfiguration().orientation); renderActionButton(data.get(selectedIndex), isDeviceLocked, mIsUdfpsEnabled); if (shouldAnimate) { animateViewsShown(mIcon, mCardLabel, mActionButton); @@ -190,6 +235,10 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard mDeviceLockedActionOnClickListener = onClickListener; } + void setShowWalletAppOnClickListener(OnClickListener onClickListener) { + mShowWalletAppOnClickListener = onClickListener; + } + void hide() { setVisibility(GONE); } @@ -206,10 +255,6 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard return mCardCarousel; } - Button getAppButton() { - return mAppButton; - } - Button getActionButton() { return mActionButton; } @@ -286,4 +331,23 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard String[] rawLabel = card.getLabel().toString().split("\\n"); return rawLabel.length == 2 ? rawLabel[1] : null; } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mFalsingCollector != null) { + mFalsingCollector.onTouchEvent(ev); + } + + boolean result = super.dispatchTouchEvent(ev); + + if (mFalsingCollector != null) { + mFalsingCollector.onMotionEventComplete(); + } + + return result; + } + + public void setFalsingCollector(FalsingCollector falsingCollector) { + mFalsingCollector = falsingCollector; + } } |